首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >JWT 安全认证深度剖析:从底层原理到企业级实战落地

JWT 安全认证深度剖析:从底层原理到企业级实战落地

作者头像
果酱带你啃java
发布2026-04-14 14:40:47
发布2026-04-14 14:40:47
490
举报

JWT安全认证深度剖析:从底层原理到企业级实战落地

引言

在分布式系统和微服务架构普及的今天,传统的Session-Cookie认证机制逐渐暴露出短板:Session依赖服务端存储,分布式环境下需要Redis等中间件实现共享,跨域场景下Cookie的传输受限,移动端适配也存在诸多不便。而JSON Web Token(JWT)凭借无状态、自包含、跨域友好的特性,成为解决这些问题的主流方案。本文将从底层原理到企业级实战,全方位拆解JWT的设计逻辑、安全机制与落地实践,让你既能吃透原理,又能直接应用到生产环境。

一、JWT核心概念与底层结构

1.1 什么是JWT?

JWT(JSON Web Token)是一种紧凑的、自包含的安全传输协议,通过JSON格式在各方之间传递信息。它的核心价值在于:信息本身可验证——接收方通过签名就能确认数据未被篡改,无需依赖第三方存储。

1.2 JWT的三段式结构

JWT由三部分组成,用.分隔,格式为Header.Payload.Signature。例如:

代码语言:javascript
复制
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
(1)Header(头部)

Header用于描述JWT的元数据,包含两个核心字段:

  • alg:签名算法(如HS256(HMAC-SHA256)、RS256(RSA-SHA256))
  • typ:令牌类型,固定为JWT

示例Header(JSON):

代码语言:javascript
复制
{
  "alg": "HS256",
  "typ": "JWT"
}

最终会被Base64Url编码为第一段字符串。

(2)Payload(载荷)

Payload是JWT的核心数据区,包含声明(Claims)——即需要传递的信息。声明分为三类:

  • 标准注册声明(建议但非强制):
    • iss:签发者(Issuer)
    • exp:过期时间(Expiration Time),必须是NumericDate格式(Unix时间戳)
    • sub:主题(Subject)
    • aud:受众(Audience)
    • iat:签发时间(Issued At)
  • 公共声明:自定义字段,需避免冲突(可参考IANA JSON Web Token Claims注册)
  • 私有声明:服务端和客户端约定的自定义字段(如用户ID、角色)

示例Payload(JSON):

代码语言:javascript
复制
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022,
  "exp": 1516242622,
  "userId": 1001,
  "role": "ADMIN"
}

同样会被Base64Url编码为第二段字符串。注意:Base64Url是可逆编码,Payload中绝对不能存放密码、令牌等敏感信息!

(3)Signature(签名)

Signature是JWT的安全核心,用于验证数据完整性和来源合法性。生成规则:

  1. 将编码后的Header和Payload用.拼接
  2. 使用Header指定的算法,结合密钥(Secret)对拼接后的字符串加密

以HS256算法为例,签名公式:

代码语言:javascript
复制
HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

最终签名结果作为第三段字符串。

服务端验证时,会重新计算签名并与接收的Signature对比:若一致,说明数据未被篡改且来源合法;若不一致,则令牌无效。

二、JWT工作原理与认证流程

2.1 核心流程

JWT的认证流程完全基于令牌传递,无需服务端存储状态,具体步骤如下:

2.2 关键细节

  • 客户端存储:JWT可存在localStorage(前端)、cookie(Web)或移动端本地存储,请求时通过Authorization头(格式:Bearer <token>)携带。
  • 服务端验证:服务端只需用密钥重新计算签名,验证令牌的完整性、过期时间等,无需查询数据库或缓存,实现真正的“无状态”。

三、JWT企业级实战:SpringBoot+MyBatisPlus集成

3.1 环境搭建

(1)Maven依赖(最新稳定版)
代码语言:javascript
复制
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/>
    </parent>
    <groupId>com.jam.demo</groupId>
    <artifactId>jwt-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>jwt-demo</name>
    <description>JWT实战演示项目</description>
    
    <properties>
        <java.version>17</java.version>
    </properties>
    
    <dependencies>
        <!-- SpringBoot Web核心 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.30</version>
            <scope>provided</scope>
        </dependency>
        
        <!-- JWT核心依赖 -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.5</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.5</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.11.5</version>
            <scope>runtime</scope>
        </dependency>
        
        <!-- MyBatisPlus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.5</version>
        </dependency>
        
        <!-- MySQL驱动 -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>8.2.0</version>
            <scope>runtime</scope>
        </dependency>
        
        <!-- 参数校验 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        
        <!-- Swagger3 -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>2.2.0</version>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
(2)配置文件(application.yml)
代码语言:javascript
复制
spring:
  # 数据库配置
datasource:
    url:jdbc:mysql://localhost:3306/jwt_demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
    username:root
    password:root
    driver-class-name:com.mysql.cj.jdbc.Driver

# 日志配置
logging:
    level:
      com.jam.demo.mapper:debug

# MyBatisPlus配置
mybatis-plus:
configuration:
    map-underscore-to-camel-case:true
    log-impl:org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
    db-config:
      id-type:auto

# JWT自定义配置
jwt:
secret:76a9f2a89e7d4b3a8f2d7c6b5a4s3d2f1g7h8j9k0l1p2o3i4u5y6t7r8e9w0q# 生产环境需用强密钥(建议256位以上)
expiration:3600000# 令牌过期时间(1小时,单位:毫秒)
refresh-expiration:86400000# 刷新令牌过期时间(24小时)
(3)数据库表设计(MySQL 8.0)
代码语言:javascript
复制
CREATE DATABASEIFNOTEXISTS jwt_demo DEFAULTCHARACTERSET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE jwt_demo;

CREATETABLE`user` (
`id`bigintNOTNULL AUTO_INCREMENT COMMENT'主键ID',
`username`varchar(50) NOTNULLCOMMENT'用户名',
`password`varchar(100) NOTNULLCOMMENT'密码(BCrypt加密)',
`role`varchar(20) NOTNULLCOMMENT'角色(ADMIN/USER)',
`create_time` datetime DEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',
`update_time` datetime DEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'更新时间',
  PRIMARY KEY (`id`),
UNIQUEKEY`uk_username` (`username`)
) ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- 插入测试数据(密码:123456)
INSERTINTO`user` (`username`, `password`, `role`) 
VALUES ('admin', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', 'ADMIN'),
       ('user', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', 'USER');

3.2 核心代码实现

(1)JWT工具类(JwtUtil.java)
代码语言:javascript
复制
package com.jam.demo.util;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.crypto.SecretKey;
import java.util.Date;
import java.util.Map;

/**
 * JWT工具类,提供令牌生成、验证、解析功能
 * @author ken
 */
@Component
@Slf4j
publicclass JwtUtil {
    /**
     * JWT签名密钥
     */
    @Value("${jwt.secret}")
    private String secret;
    
    /**
     * 访问令牌过期时间(毫秒)
     */
    @Value("${jwt.expiration}")
    privatelong expiration;
    
    /**
     * 刷新令牌过期时间(毫秒)
     */
    @Value("${jwt.refresh-expiration}")
    privatelong refreshExpiration;

    /**
     * 生成访问令牌
     * @param claims 自定义声明
     * @return JWT令牌
     */
    public String generateAccessToken(Map<String, Object> claims) {
        return generateToken(claims, expiration);
    }

    /**
     * 生成刷新令牌
     * @param claims 自定义声明
     * @return 刷新令牌
     */
    public String generateRefreshToken(Map<String, Object> claims) {
        return generateToken(claims, refreshExpiration);
    }

    /**
     * 生成JWT令牌
     * @param claims 自定义声明
     * @param expiration 过期时间(毫秒)
     * @return JWT令牌
     */
    private String generateToken(Map<String, Object> claims, long expiration) {
        SecretKey key = Keys.hmacShaKeyFor(secret.getBytes());
        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + expiration))
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();
    }

    /**
     * 解析JWT令牌
     * @param token JWT令牌
     * @return Claims对象
     */
    public Claims parseToken(String token) {
        if (!StringUtils.hasText(token)) {
            thrownew IllegalArgumentException("令牌不能为空");
        }
        try {
            SecretKey key = Keys.hmacShaKeyFor(secret.getBytes());
            return Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
        } catch (ExpiredJwtException e) {
            log.error("令牌已过期:{}", e.getMessage());
            thrownew RuntimeException("令牌已过期");
        } catch (UnsupportedJwtException e) {
            log.error("不支持的令牌格式:{}", e.getMessage());
            thrownew RuntimeException("不支持的令牌格式");
        } catch (MalformedJwtException e) {
            log.error("令牌格式错误:{}", e.getMessage());
            thrownew RuntimeException("令牌格式错误");
        } catch (SignatureException e) {
            log.error("令牌签名无效:{}", e.getMessage());
            thrownew RuntimeException("令牌签名无效");
        } catch (IllegalArgumentException e) {
            log.error("令牌参数错误:{}", e.getMessage());
            thrownew RuntimeException("令牌参数错误");
        }
    }

    /**
     * 验证令牌是否有效
     * @param token JWT令牌
     * @return true-有效,false-无效
     */
    public boolean validateToken(String token) {
        try {
            parseToken(token);
            returntrue;
        } catch (Exception e) {
            returnfalse;
        }
    }

    /**
     * 从令牌中获取用户ID
     * @param token JWT令牌
     * @return 用户ID
     */
    public Long getUserIdFromToken(String token) {
        Claims claims = parseToken(token);
        return claims.get("userId", Long.class);
    }

    /**
     * 从令牌中获取用户名
     * @param token JWT令牌
     * @return 用户名
     */
    public String getUsernameFromToken(String token) {
        Claims claims = parseToken(token);
        return claims.get("username", String.class);
    }
}
(2)用户实体类(User.java)
代码语言:javascript
复制
package com.jam.demo.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;

import java.time.LocalDateTime;

/**
 * 用户实体类
 * @author ken
 */
@Data
@TableName("user")
publicclass User {
    /**
     * 主键ID
     */
    @TableId(type = IdType.AUTO)
    private Long id;
    
    /**
     * 用户名
     */
    private String username;
    
    /**
     * 密码(BCrypt加密)
     */
    private String password;
    
    /**
     * 角色(ADMIN/USER)
     */
    private String role;
    
    /**
     * 创建时间
     */
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    
    /**
     * 更新时间
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
}
(3)MyBatisPlus填充器(MyMetaObjectHandler.java)
代码语言:javascript
复制
package com.jam.demo.config;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

/**
 * MyBatisPlus字段自动填充
 * @author ken
 */
@Component
@Slf4j
publicclass MyMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("开始插入填充...");
        this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
        this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("开始更新填充...");
        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }
}
(4)用户Mapper(UserMapper.java)
代码语言:javascript
复制
package com.jam.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.User;
import org.apache.ibatis.annotations.Mapper;

/**
 * 用户Mapper接口
 * @author ken
 */
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
(5)用户服务接口与实现(UserService.java)
代码语言:javascript
复制
package com.jam.demo.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.jam.demo.entity.User;
import com.jam.demo.vo.LoginVo;
import com.jam.demo.vo.TokenVo;

/**
 * 用户服务接口
 * @author ken
 */
publicinterface UserService extends IService<User> {
    /**
     * 用户登录
     * @param loginVo 登录参数
     * @return 令牌信息
     */
    TokenVo login(LoginVo loginVo);

    /**
     * 刷新令牌
     * @param refreshToken 刷新令牌
     * @return 新的令牌信息
     */
    TokenVo refreshToken(String refreshToken);
}
代码语言:javascript
复制
package com.jam.demo.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.common.collect.Maps;
import com.jam.demo.entity.User;
import com.jam.demo.mapper.UserMapper;
import com.jam.demo.service.UserService;
import com.jam.demo.util.JwtUtil;
import com.jam.demo.vo.LoginVo;
import com.jam.demo.vo.TokenVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

/**
 * 用户服务实现类
 * @author ken
 */
@Service
@Slf4j
publicclass UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    @Autowired
    private JwtUtil jwtUtil;

    /**
     * BCrypt密码编码器
     */
    privatefinal BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

    @Override
    public TokenVo login(LoginVo loginVo) {
        // 参数校验
        String username = loginVo.getUsername();
        String password = loginVo.getPassword();
        StringUtils.hasText(username, "用户名不能为空");
        StringUtils.hasText(password, "密码不能为空");

        // 查询用户
        User user = this.getOne(new LambdaQueryWrapper<User>().eq(User::getUsername, username));
        if (user == null) {
            thrownew RuntimeException("用户名不存在");
        }

        // 验证密码
        if (!passwordEncoder.matches(password, user.getPassword())) {
            thrownew RuntimeException("密码错误");
        }

        // 构建JWT声明
        Map<String, Object> claims = Maps.newHashMap();
        claims.put("userId", user.getId());
        claims.put("username", user.getUsername());
        claims.put("role", user.getRole());

        // 生成令牌
        String accessToken = jwtUtil.generateAccessToken(claims);
        String refreshToken = jwtUtil.generateRefreshToken(claims);

        // 返回令牌信息
        TokenVo tokenVo = new TokenVo();
        tokenVo.setAccessToken(accessToken);
        tokenVo.setRefreshToken(refreshToken);
        tokenVo.setExpiresIn(jwtUtil.getExpiration() / 1000); // 秒级返回

        log.info("用户{}登录成功,生成令牌", username);
        return tokenVo;
    }

    @Override
    public TokenVo refreshToken(String refreshToken) {
        // 验证刷新令牌
        if (!jwtUtil.validateToken(refreshToken)) {
            thrownew RuntimeException("刷新令牌无效");
        }

        // 解析刷新令牌获取用户信息
        Long userId = jwtUtil.getUserIdFromToken(refreshToken);
        String username = jwtUtil.getUsernameFromToken(refreshToken);
        User user = this.getById(userId);
        if (user == null) {
            thrownew RuntimeException("用户不存在");
        }

        // 重新生成访问令牌
        Map<String, Object> claims = Maps.newHashMap();
        claims.put("userId", user.getId());
        claims.put("username", user.getUsername());
        claims.put("role", user.getRole());

        String newAccessToken = jwtUtil.generateAccessToken(claims);
        TokenVo tokenVo = new TokenVo();
        tokenVo.setAccessToken(newAccessToken);
        tokenVo.setRefreshToken(refreshToken); // 刷新令牌未过期可复用
        tokenVo.setExpiresIn(jwtUtil.getExpiration() / 1000);

        log.info("用户{}刷新令牌成功", username);
        return tokenVo;
    }
}
(6)VO类(LoginVo.java & TokenVo.java)
代码语言:javascript
复制
package com.jam.demo.vo;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

import jakarta.validation.constraints.NotBlank;

/**
 * 登录请求VO
 * @author ken
 */
@Data
@Schema(description = "登录请求参数")
publicclass LoginVo {
    @NotBlank(message = "用户名不能为空")
    @Schema(description = "用户名")
    private String username;

    @NotBlank(message = "密码不能为空")
    @Schema(description = "密码")
    private String password;
}
代码语言:javascript
复制
package com.jam.demo.vo;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

/**
 * 令牌返回VO
 * @author ken
 */
@Data
@Schema(description = "令牌信息")
publicclass TokenVo {
    @Schema(description = "访问令牌")
    private String accessToken;

    @Schema(description = "刷新令牌")
    private String refreshToken;

    @Schema(description = "访问令牌过期时间(秒)")
    private Long expiresIn;
}
(7)JWT拦截器(JwtInterceptor.java)
代码语言:javascript
复制
package com.jam.demo.interceptor;

import com.jam.demo.util.JwtUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;

/**
 * JWT拦截器,验证请求令牌
 * @author ken
 */
@Component
@Slf4j
publicclass JwtInterceptor implements HandlerInterceptor {
    @Autowired
    private JwtUtil jwtUtil;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 获取请求头中的令牌
        String token = request.getHeader("Authorization");
        if (StringUtils.hasText(token) && token.startsWith("Bearer ")) {
            token = token.substring(7); // 截取Bearer后的令牌
        }

        // 验证令牌
        if (!jwtUtil.validateToken(token)) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write("Unauthorized: 令牌无效或已过期");
            returnfalse;
        }

        // 将用户信息存入ThreadLocal(可选,便于业务层获取)
        Long userId = jwtUtil.getUserIdFromToken(token);
        String username = jwtUtil.getUsernameFromToken(token);
        request.setAttribute("userId", userId);
        request.setAttribute("username", username);

        returntrue;
    }
}
(8)Web配置类(WebConfig.java)
代码语言:javascript
复制
package com.jam.demo.config;

import com.jam.demo.interceptor.JwtInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * Web配置类,注册拦截器
 * @author ken
 */
@Configuration
publicclass WebConfig implements WebMvcConfigurer {
    @Autowired
    private JwtInterceptor jwtInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor)
                .addPathPatterns("/api/**") // 需要拦截的路径
                .excludePathPatterns("/api/auth/login", "/api/auth/refresh"); // 排除登录和刷新令牌接口
    }
}
(9)Swagger3配置(SwaggerConfig.java)
代码语言:javascript
复制
package com.jam.demo.config;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Swagger3配置类
 * @author ken
 */
@Configuration
publicclass SwaggerConfig {
    @Bean
    public OpenAPI openAPI() {
        returnnew OpenAPI()
                .info(new Info()
                        .title("JWT认证接口文档")
                        .description("JWT实战项目的API接口文档")
                        .version("1.0.0"));
    }
}
(10)控制器(AuthController.java & TestController.java)
代码语言:javascript
复制
package com.jam.demo.controller;

import com.jam.demo.service.UserService;
import com.jam.demo.vo.LoginVo;
import com.jam.demo.vo.TokenVo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import jakarta.validation.Valid;

/**
 * 认证控制器
 * @author ken
 */
@RestController
@RequestMapping("/api/auth")
@Tag(name = "认证接口", description = "登录、刷新令牌")
@Slf4j
publicclass AuthController {
    @Autowired
    private UserService userService;

    @PostMapping("/login")
    @Operation(summary = "用户登录", description = "验证用户名密码并返回令牌")
    public TokenVo login(@Valid @RequestBody LoginVo loginVo) {
        return userService.login(loginVo);
    }

    @PostMapping("/refresh")
    @Operation(summary = "刷新令牌", description = "用刷新令牌获取新的访问令牌")
    public TokenVo refreshToken(@RequestParam String refreshToken) {
        return userService.refreshToken(refreshToken);
    }
}
代码语言:javascript
复制
package com.jam.demo.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 测试控制器(需认证)
 * @author ken
 */
@RestController
@RequestMapping("/api/test")
@Tag(name = "测试接口", description = "需要JWT认证的测试接口")
publicclass TestController {
    @GetMapping("/info")
    @Operation(summary = "获取用户信息", description = "获取当前登录用户的信息")
    public String getInfo(HttpServletRequest request) {
        Long userId = (Long) request.getAttribute("userId");
        String username = (String) request.getAttribute("username");
        return String.format("用户ID:%s,用户名:%s,认证成功!", userId, username);
    }
}

3.3 测试验证

(1)启动项目

访问Swagger3文档地址:http://localhost:8080/swagger-ui/index.html,可看到所有接口。

(2)登录接口测试

调用/api/auth/login接口,传入参数:

代码语言:javascript
复制
{
  "username": "admin",
  "password": "123456"
}

返回结果:

代码语言:javascript
复制
{
  "accessToken": "eyJhbGciOiJIUzI1NiJ9...",
  "refreshToken": "eyJhbGciOiJIUzI1NiJ9...",
  "expiresIn": 3600
}
(3)测试认证接口

调用/api/test/info接口,请求头添加Authorization: Bearer <accessToken>,返回:

代码语言:javascript
复制
用户ID:1,用户名:admin,认证成功!
(4)刷新令牌测试

调用/api/auth/refresh接口,传入刷新令牌,返回新的访问令牌。

四、JWT安全最佳实践

JWT的安全性完全依赖于实现细节,以下是企业级开发必须遵守的最佳实践:

4.1 密钥管理

  • 使用强密钥:HS256算法需至少256位(32字节)的密钥,可通过Keys.secretKeyFor(SignatureAlgorithm.HS256)生成安全密钥。
  • 密钥隔离存储:生产环境密钥需存放在配置中心(如Nacos)或环境变量,禁止硬编码。
  • 非对称加密优先:分布式系统推荐使用RS256(RSA非对称加密),私钥存服务端,公钥对外暴露,降低密钥泄露风险。

4.2 令牌安全

  • 设置合理过期时间:访问令牌(Access Token)建议1小时内,刷新令牌(Refresh Token)建议24小时,避免令牌被盗用后长期有效。
  • HTTPS传输:所有JWT传输必须通过HTTPS,防止中间人攻击窃取令牌。
  • 避免敏感数据:Payload仅存放非敏感信息(如用户ID、角色),密码、手机号等敏感数据绝对不能放入。
  • 令牌存储安全:Web端优先使用HttpOnly+Secure的Cookie存储(防止XSS攻击),localStorage易受XSS攻击;移动端存放在安全的本地存储。

4.3 验证机制

  • 强制验证所有字段:必须验证exp(过期时间)、iss(签发者)、aud(受众)等声明,避免无效令牌。
  • 防重放攻击:可结合Redis维护黑名单(如注销的令牌),验证时先查黑名单。
  • 签名验证不可跳过:绝对不能关闭签名验证,否则令牌可被任意篡改。

五、JWT vs 传统Session认证

特性

JWT认证

Session认证

存储位置

客户端(localStorage/cookie)

服务端(内存/Redis)

状态

无状态(服务端无需存储)

有状态(服务端需维护Session)

分布式支持

天然支持(无需共享)

需Redis等中间件共享Session

跨域支持

友好(令牌可跨域传递)

受限(Cookie跨域需配置)

性能

高(无数据库/缓存查询)

中(需查询Session)

扩展性

强(支持多端)

弱(依赖Cookie)

安全性

依赖实现(需HTTPS+强密钥)

较高(Cookie可设HttpOnly)

令牌撤销

复杂(需黑名单)

简单(直接删除Session)

六、企业级扩展方案

6.1 刷新令牌机制

访问令牌短期有效,刷新令牌长期有效。当访问令牌过期时,客户端用刷新令牌获取新的访问令牌,避免用户频繁登录。实现要点:

  • 刷新令牌需单独存储(如Redis),并设置过期时间。
  • 刷新令牌使用后可失效(一次性),防止重复使用。

6.2 多端适配

  • Web端:Cookie(HttpOnly+Secure)或localStorage(配合CSRF防护)。
  • 移动端:本地安全存储(如Android的Keystore,iOS的Keychain)。
  • 第三方应用:OAuth2.0+JWT集成,实现授权登录。

6.3 令牌撤销

JWT本身无法主动撤销,企业级需结合Redis实现黑名单:

  • 用户注销时,将令牌存入Redis黑名单,设置过期时间与令牌一致。
  • 验证令牌时,先检查是否在黑名单中。

七、总结

JWT作为无状态认证方案,完美解决了分布式系统的认证痛点,但它并非银弹——需严格遵守安全最佳实践,才能发挥其优势。本文从原理到实战,覆盖了JWT的核心知识和企业级落地细节,你可直接基于示例代码搭建生产级认证系统。记住:JWT的安全性在于细节,强密钥、HTTPS、合理的过期时间是保障系统安全的基石。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-02-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 果酱带你啃java 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • JWT安全认证深度剖析:从底层原理到企业级实战落地
    • 引言
    • 一、JWT核心概念与底层结构
      • 1.1 什么是JWT?
      • 1.2 JWT的三段式结构
    • 二、JWT工作原理与认证流程
      • 2.1 核心流程
      • 2.2 关键细节
    • 三、JWT企业级实战:SpringBoot+MyBatisPlus集成
      • 3.1 环境搭建
      • 3.2 核心代码实现
      • 3.3 测试验证
    • 四、JWT安全最佳实践
      • 4.1 密钥管理
      • 4.2 令牌安全
      • 4.3 验证机制
    • 五、JWT vs 传统Session认证
    • 六、企业级扩展方案
      • 6.1 刷新令牌机制
      • 6.2 多端适配
      • 6.3 令牌撤销
    • 七、总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档