(八)从“认证混乱难管控”到“JWT高效赋能”——JWT实战进阶指南

引言:入职8个月,被"用户认证"逼到emo

搞定Spring Boot、MySQL、MongoDB后,我以为自己能轻松应对后端开发的各类场景,可当项目进入用户认证环节,我又一次陷入了困境。最初用Session实现用户认证,看似简单,却藏着一堆麻烦:

  • 分布式部署坑:Session存在服务器内存中,多台服务器部署时,Session无法共享,用户登录后切换服务器就会被强制登出,只能用Redis存储Session,额外增加开发和维护成本;

  • 扩展性差:移动端(APP、小程序)没有Cookie,无法携带SessionID,适配多端认证时,需要额外开发适配逻辑;

  • 安全性不足:SessionID存放在Cookie中,容易被CSRF攻击、Session劫持,且无法精准控制token的有效期和权限范围;

  • 性能损耗:每次请求都要查询Redis或服务器内存校验Session,高并发场景下,会增加服务器压力,影响接口响应速度。

有一次,项目上线分布式部署后,用户反馈"登录后频繁掉线",排查了整整一天,才发现是Session未共享导致的------每台服务器都有自己的Session,用户请求切换到其他服务器后,找不到对应的Session,就会被判定为未登录。旁边的李哥看我焦头烂额,笑着说:"小王,Session只适合单体项目,现在分布式、多端开发是主流,JWT才是用户认证的'最优解'。"

李哥告诉我,JWT(JSON Web Token)是一种无状态的认证方式,不需要在服务器存储用户信息,只需通过一段加密的Token,就能实现用户身份校验、权限控制,完美解决Session的所有痛点,适配分布式、多端开发场景。

那天之后,我彻底投入JWT的学习,从最基础的核心概念、加密原理,到Spring Boot整合JWT、实战落地、权限控制,再到高级优化、安全防护、故障排查,一步步摆脱了"认证混乱"的困境,慢慢学会了用JWT为项目构建安全、高效的认证体系。今天,就把这段从"被认证问题折磨"到"掌控JWT全流程"的成长历程,分享给和曾经的我一样,被用户认证困扰、想拓展技术边界的Java新手开发者。

注:本文聚焦JWT 4.x(当前主流稳定版本),结合Spring Boot 3.2.x整合JWT的实战场景,不冗余讲解过时用法,全程用真实业务场景(用户登录、接口认证、权限管控)串联知识点,从入门配置到高级技巧,从新手踩坑到规范落地,融入实战代码,避开枯燥说教,让你看完就能上手,真正把JWT用在日常开发中,实现"认证流程简化、系统安全升级"。


第一章:入门破局------吃透JWT基础,告别认证混乱

李哥告诉我:"JWT入门很简单,核心就是'无状态认证'------把用户信息加密到一段Token中,客户端每次请求都携带这段Token,服务器只需解密Token,就能完成身份校验,无需存储任何用户信息,完美适配分布式场景。新手不用一开始就追求复杂用法,先把基础的核心概念、加密原理、Spring Boot整合搞懂,就能解决大部分认证场景的需求。"

对于Java新手来说,JWT的进阶第一步,就是吃透以下基础知识点,快速摆脱认证混乱的困扰。

一、先搞懂:JWT到底是什么?(趣味类比,一看就懂)

很多新手一听到JWT,就会和Session、Cookie混淆,甚至觉得"有了Session,就不用学JWT了",其实这是一个很大的误区。一句话讲明白JWT的定位:JWT是一种无状态的JSON Web令牌,用于在客户端和服务器之间安全地传递用户身份信息和权限信息,它不替代Session,而是解决Session在分布式、多端场景下的短板,成为当前主流的认证方式。

举个通俗的例子:如果把Session比作"小区门禁卡",你每次进入小区,保安都要查门禁卡(SessionID),还要去物业系统(服务器内存/Redis)核对你的信息,确认你是小区业主;而JWT就像"身份证",身份证上直接印着你的姓名、身份证号(用户信息),还有防伪标识(加密签名),保安只需核对身份证的防伪标识(解密Token),就能确认你的身份,无需再去物业系统查询------这就是"无状态认证"的核心优势。

补充:JWT的核心优势有4点------

  1. 无状态:服务器无需存储用户信息,只需解密Token即可完成校验,减轻服务器压力,完美适配分布式部署;

  2. 多端适配:Token可通过请求头、URL、请求体携带,无需依赖Cookie,适配APP、小程序、PC端等多端场景;

  3. 安全性高:支持多种加密算法(HS256、RS256等),Token具有签名防伪机制,篡改后会直接失效;

  4. 可扩展性强:Token中可携带自定义信息(如用户ID、角色、权限),无需额外查询数据库,提升接口响应速度。

这也是为什么现在大部分分布式项目、多端项目,都会选择JWT作为用户认证方式。

二、JWT核心结构(3部分组成,一看就懂)

JWT的Token由3部分组成,用英文句号(.)分隔,格式为:Header.Payload.Signature,每一部分都有明确的作用,新手无需死记硬背,理解每部分的含义即可。

1. Header(头部)

Header主要包含两部分信息:令牌类型(typ)和加密算法(alg),默认是HS256(对称加密),也可使用RS256(非对称加密)。

示例(JSON格式):

groovy 复制代码
{
  "typ": "JWT", // 令牌类型,固定为JWT
  "alg": "HS256" // 加密算法,HS256为对称加密,RS256为非对称加密
}

Header会被Base64编码(注意:Base64是编码,不是加密,可直接解码,因此不能存放敏感信息),作为Token的第一部分。

2. Payload(载荷)

Payload是JWT的核心部分,用于存放用户身份信息、权限信息等自定义数据,也可以存放JWT的标准声明(可选)。

(1)标准声明(常用3个,无需自定义)
  • iss(issuer):签发人,即JWT的签发者(如项目名称);

  • exp(expiration time):过期时间,即Token的有效期(如1小时),格式为时间戳;

  • iat(issued at):签发时间,即Token的创建时间,格式为时间戳。

(2)自定义声明(实战常用)

根据业务需求,存放用户相关的非敏感信息,如用户ID、用户名、角色、权限等,示例:

groovy 复制代码
{
  "iss": "demo-project", // 签发人
  "exp": 1718985600, // 过期时间(时间戳)
  "iat": 1718982000, // 签发时间(时间戳)
  "userId": 1001, // 自定义:用户ID
  "userName": "zhangsan", // 自定义:用户名
  "role": "admin" // 自定义:用户角色
}

⚠️ 重要提醒:Payload会被Base64编码,可直接解码查看,因此绝对不能存放敏感信息(如密码、手机号、身份证号等),只能存放非敏感的用户标识和权限信息。

3. Signature(签名)

Signature是JWT的"防伪标识",用于验证Token是否被篡改,确保Token的安全性,也是JWT最核心的安全保障。

(1)签名生成逻辑(以HS256对称加密为例)
  1. 将Header和Payload分别进行Base64编码,得到编码后的字符串;

  2. 用英文句号(.)连接编码后的Header和Payload,得到 Header编码.Payload编码;

  3. 使用提前约定好的密钥(secret),对上述字符串进行HS256加密,得到签名;

  4. 将签名作为Token的第三部分,与Header编码、Payload编码用句号连接,得到完整的JWT Token。

(2)签名验证逻辑

服务器收到客户端携带的Token后,会执行以下步骤验证Token的有效性:

  1. 将Token按句号分隔,得到Header编码、Payload编码、Signature;

  2. 对Header编码、Payload编码进行Base64解码,获取加密算法(alg)和用户信息;

  3. 使用相同的密钥(secret),对 Header编码.Payload编码进行加密,得到新的签名;

  4. 将新的签名与客户端携带的Signature进行对比,若一致,说明Token未被篡改;若不一致,说明Token被篡改,直接拒绝请求;

  5. 验证Payload中的exp(过期时间),若当前时间超过过期时间,说明Token已失效,直接拒绝请求。

三、JWT vs Session(对比理解,避免混淆)

新手很容易混淆JWT和Session,通过以下表格对比,能快速理解两者的区别和适用场景,避免用错场景:

对比维度 JWT Session
存储位置 客户端(Token携带) 服务器(内存/Redis)
状态 无状态(服务器不存储用户信息) 有状态(服务器需存储Session信息)
分布式适配 完美适配(无需共享Token) 需额外处理(如Redis共享Session)
多端适配 适配(APP、小程序、PC端均可携带) 不适配(依赖Cookie)
安全性 高(签名防伪,篡改失效) 较低(易被CSRF、Session劫持)
性能 高(无需查询服务器存储) 较低(需查询Session存储)
适用场景 分布式项目、多端项目、API接口认证 单体项目、简单场景

总结:单体项目可用Session,分布式、多端项目优先用JWT,这是当前后端开发的主流选择。

四、环境搭建(Spring Boot整合JWT,可直接复制)

新手最容易踩的坑:JWT依赖版本不兼容、密钥配置错误、Token生成/解析逻辑繁琐,导致认证失败。其实Spring Boot整合JWT非常简单,只需3步:导入依赖、配置参数、编写JWT工具类,新手可直接复制粘贴。

1. Maven依赖(pom.xml中添加)

使用当前主流的JWT 4.x版本,完美适配Spring Boot 3.2.x,无需手动配置版本冲突:

xml 复制代码
<!-- JWT核心依赖(4.x版本) -->
<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>

<!-- 可选:lombok简化代码 -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

<!-- Spring Web依赖(若项目未导入) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Spring Security依赖(可选,用于权限控制,后续章节会用到) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

2. 配置文件(application.yml)

配置JWT核心参数(密钥、过期时间、签发人),新手直接修改参数值即可,无需修改配置结构:

yml 复制代码
spring:
  application:
    name: jwt-demo

# JWT核心配置(自定义配置)
jwt:
  # 密钥(重要!建议生产环境使用复杂密钥,如UUID,避免泄露)
  secret: 7a8b9c0d1e2f3g4h5i6j7k8l9m0n1o2p3q4r5s6t7u8v9w0x
  # Token过期时间(单位:毫秒),这里设置1小时(3600000毫秒)
  expiration: 3600000
  # 签发人(可自定义,如项目名称)
  issuer: jwt-demo-project
  # Token携带的请求头名称(自定义,如Authorization)
  header: Authorization
  # Token前缀(自定义,如Bearer,注意后面有空格)
  prefix: Bearer 

3. 关键配置说明(新手必看)

  • 密钥(secret):核心安全保障,必须复杂且保密,生产环境建议使用UUID或随机字符串,避免简单字符串(如123456),否则容易被破解;

  • 过期时间(expiration):根据业务需求设置,一般为1-24小时,过期时间过短会导致用户频繁登录,过长会增加安全风险;

  • Token前缀(prefix):建议添加(如Bearer),用于区分Token和其他请求头信息,服务器解析时需先去掉前缀;

  • 请求头名称(header):自定义即可,常用Authorization,客户端请求时需将Token放在该请求头中。

4. 编写JWT工具类(核心,可直接复制)

JWT工具类封装Token的生成、解析、验证等核心方法,无需重复编写代码,Spring Boot可直接注入使用:

java 复制代码
package com.example.jwtdemo.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 javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;

/**
 * JWT工具类(Spring Boot 3.2.x 适配版)
 * 封装Token生成、解析、验证核心方法
 */
@Slf4j
@Component
public class JwtUtil {

    // 从配置文件中读取JWT密钥
    @Value("${jwt.secret}")
    private String secret;

    // 从配置文件中读取Token过期时间(毫秒)
    @Value("${jwt.expiration}")
    private Long expiration;

    // 从配置文件中读取签发人
    @Value("${jwt.issuer}")
    private String issuer;

    /**
     * 生成JWT Token
     * @param claims 自定义声明(存放用户ID、角色等非敏感信息)
     * @return 完整的JWT Token
     */
    public String generateToken(Map<String, Object> claims) {
        // 1. 生成密钥(根据配置文件中的secret字符串,转换为JWT所需的SecretKey)
        SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));

        // 2. 构建Token
        return Jwts.builder()
                .setClaims(claims) // 设置自定义声明
                .setIssuer(issuer) // 设置签发人
                .setIssuedAt(new Date()) // 设置签发时间
                .setExpiration(new Date(System.currentTimeMillis() + expiration)) // 设置过期时间
                .signWith(key, SignatureAlgorithm.HS256) // 设置加密算法和密钥
                .compact(); // 生成Token
    }

    /**
     * 解析JWT Token,获取自定义声明
     * @param token JWT Token(不含前缀)
     * @return 自定义声明(Map形式)
     */
    public Map<String, Object> parseToken(String token) {
        try {
            SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
            // 解析Token,获取Claims(包含标准声明和自定义声明)
            Claims claims = Jwts.parserBuilder()
                    .setSigningKey(key) // 设置密钥,用于验证签名
                    .build()
                    .parseClaimsJws(token) // 解析Token
                    .getBody(); // 获取Payload中的Claims
            return claims;
        } catch (Exception e) {
            log.error("JWT Token解析失败:{}", e.getMessage());
            throw new RuntimeException("Token无效或已过期");
        }
    }

    /**
     * 验证JWT Token是否有效
     * @param token JWT Token(不含前缀)
     * @return true:有效,false:无效
     */
    public boolean validateToken(String token) {
        try {
            SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
            // 解析Token,若解析失败(如篡改、过期),会抛出异常
            Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(token);
            return true;
        } catch (ExpiredJwtException e) {
            log.error("JWT Token已过期:{}", e.getMessage());
        } catch (MalformedJwtException e) {
            log.error("JWT Token格式错误:{}", e.getMessage());
        } catch (SignatureException e) {
            log.error("JWT Token签名异常(可能被篡改):{}", e.getMessage());
        } catch (Exception e) {
            log.error("JWT Token验证失败:{}", e.getMessage());
        }
        return false;
    }

    /**
     * 从Token中获取用户ID(自定义方法,根据业务需求调整)
     * @param token JWT Token(不含前缀)
     * @return 用户ID
     */
    public Long getUserIdFromToken(String token) {
        Map<String, Object> claims = parseToken(token);
        // 注意:这里的"userId"要和生成Token时存入的自定义声明key一致
        return Long.parseLong(claims.get("userId").toString());
    }

    /**
     * 从Token中获取用户角色(自定义方法,根据业务需求调整)
     * @param token JWT Token(不含前缀)
     * @return 用户角色
     */
    public String getRoleFromToken(String token) {
        Map<String, Object> claims = parseToken(token);
        return claims.get("role").toString();
    }
}

5. 工具类核心方法说明(新手必懂)

  • generateToken:生成Token,传入自定义声明(如用户ID、角色),返回完整的Token字符串;

  • parseToken:解析Token,返回自定义声明,若Token无效/过期,会抛出异常;

  • validateToken:验证Token有效性,返回布尔值,捕获各类异常(过期、篡改、格式错误);

  • getUserIdFromToken、getRoleFromToken:自定义方法,从Token中快速获取用户ID、角色,可根据业务需求新增(如获取用户名)。

五、核心:JWT实战落地(Spring Boot,可直接复制)

掌握了环境搭建和工具类编写后,就可以结合真实业务场景(用户登录生成Token、接口认证校验Token),实现JWT的实战落地,新手可直接复制测试。

1. 实体类(User实体,简化版)

java 复制代码
package com.example.jwtdemo.entity;

import lombok.Data;

/**
 * 用户实体类(简化版,仅用于演示JWT认证)
 */
@Data
public class User {
    // 用户ID
    private Long userId;
    // 用户名
    private String userName;
    // 密码(实际开发中需加密存储,如BCrypt加密)
    private String password;
    // 用户角色(如admin、user)
    private String role;
}

2. Service层代码(用户登录、Token生成)

java 复制代码
package com.example.jwtdemo.service;

import com.example.jwtdemo.entity.User;
import com.example.jwtdemo.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

/**
 * 用户服务层(包含登录、Token生成逻辑)
 */
@Service
public class UserService {

    @Autowired
    private JwtUtil jwtUtil;

    // 密码加密器(Spring Security提供,用于密码加密和校验)
    private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

    /**
     * 模拟用户登录(实际开发中需查询数据库)
     * @param userName 用户名
     * @param password 密码
     * @return JWT Token
     */
    public String login(String userName, String password) {
        // 1. 模拟查询数据库,获取用户信息(实际开发中替换为MyBatis/MyBatis-Plus查询)
        User user = getMockUser(userName);
        if (user == null) {
            throw new RuntimeException("用户名不存在");
        }

        // 2. 校验密码(实际开发中,数据库存储的是加密后的密码,需用passwordEncoder.matches校验)
        if (!passwordEncoder.matches(password, user.getPassword())) {
            throw new RuntimeException("密码错误");
        }

        // 3. 构建自定义声明(存放非敏感用户信息)
        Map<String, Object> claims = new HashMap<>();
        claims.put("userId", user.getUserId());
        claims.put("userName", user.getUserName());
        claims.put("role", user.getRole());

        // 4. 生成JWT Token
        return jwtUtil.generateToken(claims);
    }

    /**
     * 模拟查询数据库,返回用户信息(实际开发中删除,替换为真实查询)
     * 注意:密码已用BCrypt加密(原始密码:123456)
     */
    private User getMockUser(String userName) {
        if ("zhangsan".equals(userName)) {
            User user = new User();
            user.setUserId(1001L);
            user.setUserName("zhangsan");
            user.setPassword("$2a$10$EixZaYbB.rK4fl8x2q7Meu6Q6D2V7X7aKQ5Q5Q5Q5Q5Q5Q5Q5Q5"); // 123456加密后
            user.setRole("admin"); // 管理员角色
            return user;
        } else if ("lisi".equals(userName)) {
            User user = new User();
            user.setUserId(1002L);
            user.setUserName("lisi");
            user.setPassword("$2a$10$EixZaYbB.rK4fl8x2q7Meu6Q6D2V7X7aKQ5Q5Q5Q5Q5Q5Q5Q5"); // 123456加密后
            user.setRole("user"); // 普通用户角色
            return user;
        }
        return null;
    }
}

3. 编写JWT拦截器(接口认证,核心)

通过拦截器拦截所有需要认证的接口,校验请求头中的Token,若Token无效/过期,直接拒绝请求;若有效,解析Token中的用户信息,存入请求上下文,供后续接口使用。

java 复制代码
package com.example.jwtdemo.interceptor;

import com.example.jwtdemo.util.JwtUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

/**
 * JWT拦截器(用于接口认证校验)
 */
@Component
public class JwtInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtUtil jwtUtil;

    // 从配置文件中读取JWT请求头名称和前缀
    @Value("${jwt.header}")
    private String jwtHeader;

    @Value("${jwt.prefix}")
    private String jwtPrefix;

    /**
     * 接口请求前拦截,校验Token
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1. 获取请求头中的Token
        String token = request.getHeader(jwtHeader);
        if (token == null || token.isEmpty()) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write("未携带Token,拒绝访问");
            return false;
        }

        // 2. 去掉Token前缀(如Bearer )
        if (!token.startsWith(jwtPrefix)) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write("Token格式错误");
            return false;
        }
        token = token.substring(jwtPrefix.length()).trim(); // 去掉前缀,获取纯Token

        // 3. 验证Token有效性
        if (!jwtUtil.validateToken(token)) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write("Token无效或已过期");
            return false;
        }

        // 4. Token有效,解析用户信息,存入请求上下文(供后续接口使用)
        Long userId = jwtUtil.getUserIdFromToken(token);
        String role = jwtUtil.getRoleFromToken(token);
        request.setAttribute("userId", userId);
        request.setAttribute("role", role);

        // 5. 放行请求
        return true;
    }
}

4. 配置拦截器(让拦截器生效)

java 复制代码
package com.example.jwtdemo.config;

import com.example.jwtdemo.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配置类,注册JWT拦截器
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private JwtInterceptor jwtInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor)
                .addPathPatterns("/api/**") // 拦截所有/api/**开头的接口(需要认证的接口)
                .excludePathPatterns("/api/login"); // 排除登录接口(无需认证)
    }
}

5. Controller层代码(接口测试)

java 复制代码
package com.example.jwtdemo.controller;

import com.example.jwtdemo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

/**
 * 接口测试控制器(登录接口+需要认证的接口)
 */
@RestController
@RequestMapping("/api")
public class AuthController {

    @Autowired
    private UserService userService;

    /**
     * 用户登录接口(无需认证)
     * @param userName 用户名
     * @param password 密码
     * @return Token和用户信息
     */
    @PostMapping("/login")
    public Map<String, Object> login(@RequestParam String userName, @RequestParam String password) {
        // 调用Service层登录方法,生成Token
        String token = userService.login(userName, password);
        Map<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("message", "登录成功");
        result.put("token", token); // 返回Token
        result.put("tokenPrefix", "Bearer"); // 返回Token前缀,供客户端使用
        return result;
    }

    /**
     * 需要认证的接口(普通用户和管理员均可访问)
     */
    @GetMapping("/user/info")
    public Map<String, Object> getUserInfo(HttpServletRequest request) {
        // 从请求上下文获取用户信息(拦截器中存入)
        Long userId = (Long) request.getAttribute("userId");
        String role = (String) request.getAttribute("role");
        Map<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("message", "查询成功");
        result.put("userId", userId);
        result.put("role", role);
        return result;
    }

    /**
     * 需要管理员权限的接口(仅admin角色可访问)
     */
    @GetMapping("/admin/operate")
    public Map<String, Object> adminOperate(HttpServletRequest request) {
        String role = (String) request.getAttribute("role");
        // 校验角色权限
        if (!"admin".equals(role)) {
            Map<String, Object> result = new HashMap<>();
            result.put("code", 403);
            result.put("message", "权限不足,仅管理员可访问");
            return result;
        }
        Map<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("message", "管理员操作成功");
        return result;
    }
}

6. 接口测试示例(Postman)

(1)用户登录(获取Token)
  • 请求地址:POST /api/login

  • 请求参数:userName=zhangsan,password=123456

  • 响应体:

groovy 复制代码
{
  "code": 200,
  "message": "登录成功",
  "token": "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEwMDEsInVzZXJOYW1lIjoiemhhbmdzYW4iLCJyb2xlIjoiYWRtaW4iLCJpc3MiOiJqd3QtZGVtby1wcm9qZWN0IiwiaWF0IjoxNzE4OTgyMDAwLCJleHAiOjE3MTg5ODU2MDB9.7a8b9c0d1e2f3g4h5i6j7k8l9m0n1o2p3q4r5s6t7u8v9w0x",
  "tokenPrefix": "Bearer"
}
(2)访问需要认证的接口(携带Token)
  • 请求地址:GET /api/user/info

  • 请求头:Authorization = Bearer + 登录返回的Token(注意空格)

  • 响应体:

groovy 复制代码
{
  "code": 200,
  "message": "查询成功",
  "userId": 1001,
  "role": "admin"
}
(3)访问管理员权限接口
  • 请求地址:GET /api/admin/operate

  • 请求头:Authorization = Bearer + 管理员Token(zhangsan的Token)

  • 响应体:

groovy 复制代码
{
  "code": 200,
  "message": "管理员操作成功"
}
(4)Token无效/过期测试
  • 请求地址:GET /api/user/info

  • 请求头:Authorization = Bearer + 无效Token(如篡改后的Token)

  • 响应体:Token无效或已过期(状态码401)


第二章:实战进阶------JWT高级用法,覆盖复杂业务场景

掌握了JWT的基础用法和实战落地后,我开始在项目中大量使用JWT实现用户认证,但很快发现,基础用法无法覆盖复杂场景------比如Token刷新、权限精细化控制、非对称加密、多端Token管理等。这时李哥告诉我:"JWT的高级用法,才是它真正的'核心竞争力',能帮你解决复杂认证场景下的各种问题,让认证体系更安全、更灵活。"

新手重点掌握以下5个高级用法,能轻松应对90%的复杂业务场景。

一、Token刷新机制(解决用户频繁登录问题)

问题场景

基础用法中,Token过期后,用户需要重新登录,体验较差;若Token过期时间设置过长,又会增加安全风险。此时需要实现Token刷新机制------当Token即将过期时,客户端请求刷新接口,获取新的Token,无需用户重新登录。

实现方案(可直接复制)

1. 优化JWT工具类,新增判断Token是否即将过期的方法
java 复制代码
/**
 * 判断Token是否即将过期(自定义阈值,如剩余时间小于300秒,即5分钟)
 * @param token JWT Token(不含前缀)
 * @param threshold 过期阈值(毫秒),如300000毫秒(5分钟)
 * @return true:即将过期,false:未即将过期
 */
public boolean isTokenAboutToExpire(String token, Long threshold) {
    Map<String, Object> claims = parseToken(token);
    Date expiration = claims.getExpiration(); // 获取Token过期时间
    Date now = new Date();
    // 计算剩余时间 = 过期时间 - 当前时间
    long remainingTime = expiration.getTime() - now.getTime();
    // 若剩余时间小于阈值,说明即将过期
    return remainingTime < threshold;
}
2. 新增Token刷新接口(Controller层)
java 复制代码
/**
 * Token刷新接口(需要携带原Token,无需用户重新登录)
 * @param request 请求对象(获取原Token)
 * @return 新的Token
 */
@PostMapping("/refreshToken")
public Map<String, Object> refreshToken(HttpServletRequest request) {
    // 1. 获取原Token
    String token = request.getHeader(jwtHeader);
    if (token == null || !token.startsWith(jwtPrefix)) {
        Map<String, Object> result = new HashMap<>();
        result.put("code", 401);
        result.put("message", "Token格式错误,无法刷新");
        return result;
    }
    token = token.substring(jwtPrefix.length()).trim();

    // 2. 验证原Token有效性
    if (!jwtUtil.validateToken(token)) {
        Map<String, Object> result = new HashMap<>();
        result.put("code", 401);
        result.put("message", "原Token无效,无法刷新");
        return result;
    }

    // 3. 判断Token是否即将过期(阈值设为5分钟,300000毫秒)
    if (!jwtUtil.isTokenAboutToExpire(token, 300000L)) {
        Map<String, Object> result = new HashMap<>();
        result.put("code", 400);
        result.put("message", "Token未即将过期,无需刷新");
        return result;
    }

    // 4. 解析原Token中的用户信息,生成新的Token(过期时间重新计算)
    Map<String, Object> claims = jwtUtil.parseToken(token);
    String newToken = jwtUtil.generateToken(claims);

    // 5. 返回新Token
    Map<String, Object> result = new HashMap<>();
    result.put("code", 200);
    result.put("message", "Token刷新成功");
    result.put("newToken", newToken);
    result.put("tokenPrefix", "Bearer");
    return result;
}
3. 客户端实现逻辑(新手参考)
  • 客户端每次请求接口后,解析响应结果,若提示Token即将过期,自动调用刷新接口,获取新Token;

  • 将新Token存储在客户端(如localStorage、Cookie),替换旧Token,后续请求使用新Token;

  • 若刷新接口返回Token无效,说明原Token已过期,引导用户重新登录。

二、JWT权限精细化控制(结合Spring Security)

基础用法中,我们通过拦截器简单校验角色,但在复杂项目中,需要更精细化的权限控制(如不同接口对应不同权限,同一接口不同用户有不同操作权限),此时结合Spring Security,能实现更灵活的权限管控。

实现方案(可直接复制)

1. 配置Spring Security(适配Spring Boot 3.2.x)
java 复制代码
package com.example.jwtdemo.config;

import com.example.jwtdemo.interceptor.JwtInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * Spring Security配置类(JWT权限控制)
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private JwtInterceptor jwtInterceptor;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                // 关闭CSRF防护(JWT无需CSRF防护)
                .csrf(csrf -> csrf.disable())
                // 关闭Session(JWT无状态认证,无需Session)
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                // 配置接口权限
                .authorizeHttpRequests(authorize -> authorize
                        // 登录接口、刷新Token接口放行
                        .requestMatchers("/api/login", "/api/refreshToken").permitAll()
                        // 管理员接口,仅admin角色可访问
                        .requestMatchers("/api/admin/**").hasRole("admin")
                        // 普通用户接口,仅user角色可访问
                        .requestMatchers("/api/user/**").hasRole("user")
                        // 其他所有接口,需要认证(登录后可访问)
                        .anyRequest().authenticated()
                )
                // 添加JWT拦截器,在UsernamePasswordAuthenticationFilter之前执行
                .addFilterBefore(jwtInterceptor, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}
2. 优化JWT拦截器,整合Spring Security上下文
java 复制代码
// 在JwtInterceptor的preHandle方法中,添加以下代码(替换原请求上下文存入逻辑)
// 4. Token有效,解析用户信息,存入Spring Security上下文(供Security权限校验使用)
Long userId = jwtUtil.getUserIdFromToken(token);
String role = jwtUtil.getRoleFromToken(token);

// 构建Spring Security的认证对象
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
        userId, // 用户名/用户ID(可自定义)
        null, // 密码(JWT无状态,无需存储密码)
        // 配置用户权限(角色需要添加ROLE_前缀,如ROLE_admin)
        Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role))
);

// 将认证对象存入Security上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
3. 权限控制示例
  • /api/admin/** 接口:仅ROLE_admin角色可访问(即role=admin);

  • /api/user/** 接口:仅ROLE_user角色可访问(即role=user);

  • 其他接口:只要登录(Token有效)即可访问,无需区分角色;

  • 若用户角色不匹配,Spring Security会自动返回403权限不足,无需手动校验。

三、非对称加密(HS256 vs RS256,提升安全性)

基础用法中,我们使用HS256对称加密------服务器和客户端使用相同的密钥(secret),若密钥泄露,攻击者可伪造Token,存在安全风险。在生产环境中,推荐使用RS256非对称加密,安全性更高。

核心区别

  • HS256(对称加密):只有一个密钥,服务器和客户端共用,加密和解密使用同一个密钥,简单高效,但密钥泄露风险高;

  • RS256(非对称加密):有两个密钥------私钥(private key)和公钥(public key),私钥仅服务器持有(用于生成Token),公钥可公开(用于解析Token),即使公钥泄露,攻击者也无法伪造Token(伪造需要私钥)。

实现方案(可直接复制)

1. 生成RSA密钥对(私钥+公钥)

通过Java代码生成RSA密钥对(也可通过OpenSSL命令生成),生成后保存到项目resources目录下:

java 复制代码
package com.example.jwtdemo.util;

import java.io.FileOutputStream;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Base64;

/**
 * RSA密钥对生成工具(生成私钥和公钥,用于RS256非对称加密)
 */
public class RsaKeyGenerator {
    public static void main(String[] args) throws Exception {
        // 1. 初始化RSA密钥生成器(密钥长度2048位,安全性足够)
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        keyPairGenerator.initialize(2048);

        // 2. 生成密钥对
        KeyPair keyPair = keyPairGenerator.generateKeyPair();
        PrivateKey privateKey = keyPair.getPrivate(); // 私钥(服务器持有,用于生成Token)
        PublicKey publicKey = keyPair.getPublic(); // 公钥(可公开,用于解析Token)

        // 3. 将私钥和公钥转换为Base64编码,便于存储和使用
        String privateKeyStr = Base64.getEncoder().encodeToString(privateKey.getEncoded());
        String publicKeyStr = Base64.getEncoder().encodeToString(publicKey.getEncoded());

        // 4. 保存私钥到文件(resources目录下)
        try (FileOutputStream fos = new FileOutputStream("src/main/resources/privateKey.txt")) {
            fos.write(privateKeyStr.getBytes(StandardCharsets.UTF_8));
        }
        // 5. 保存公钥到文件(resources目录下)
        try (FileOutputStream fos = new FileOutputStream("src/main/resources/publicKey.txt")) {
            fos.write(publicKeyStr.getBytes(StandardCharsets.UTF_8));
        }

        // 打印密钥对(便于测试,生产环境可删除)
        System.out.println("RSA私钥(Base64编码):" + privateKeyStr);
        System.out.println("RSA公钥(Base64编码):" + publicKeyStr);
        System.out.println("密钥对已保存到src/main/resources目录下");
    }
}
2. 运行说明(新手必看)
  • 直接运行此类的main方法,会在项目src/main/resources目录下生成privateKey.txt(私钥)和publicKey.txt(公钥)两个文件;

  • 生成后,私钥需妥善保管,仅服务器端使用,切勿泄露;公钥可公开,供客户端或其他服务解析Token使用;

  • 密钥长度为2048位,是当前主流的安全长度,无需修改,若需更高安全性,可调整为4096位(修改keyPairGenerator.initialize(4096)),但会增加加密解密耗时。

3. 配置文件优化(适配RS256非对称加密)

修改application.yml配置文件,替换原有的对称加密密钥(secret),新增RSA私钥、公钥配置,无需修改其他JWT核心参数:

yaml 复制代码
spring:
  application:
    name: jwt-demo

# JWT核心配置(RS256非对称加密适配版)
jwt:
  # 替换原有secret,改为RSA私钥(从privateKey.txt中复制,去掉换行)
  private-key: MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCd7...(完整私钥Base64编码)
  # RSA公钥(从publicKey.txt中复制,去掉换行)
  public-key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp3v...(完整公钥Base64编码)
  # Token过期时间(单位:毫秒),保持不变
  expiration: 3600000
  # 签发人(可自定义,如项目名称)
  issuer: jwt-demo-project
  # Token携带的请求头名称(自定义,如Authorization)
  header: Authorization
  # Token前缀(自定义,如Bearer,注意后面有空格)
  prefix: Bearer 

⚠️ 注意:私钥和公钥复制时,需去掉文件中的换行符,确保配置为一行完整的Base64编码字符串,否则会导致密钥解析失败。

4. 优化JWT工具类(适配RS256非对称加密)

替换原有的HS256对称加密逻辑,改为RS256非对称加密,核心修改密钥解析、Token生成和解析方法,其余方法保持不变,可直接替换原有JwtUtil类:

java 复制代码
package com.example.jwtdemo.util;

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

import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.Map;

/**
 * JWT工具类(Spring Boot 3.2.x 适配版,RS256非对称加密)
 * 封装Token生成、解析、验证核心方法,替换原有HS256对称加密逻辑
 */
@Slf4j
@Component
public class JwtUtil {

    // 从配置文件中读取RSA私钥(Base64编码)
    @Value("${jwt.private-key}")
    private String privateKeyStr;

    // 从配置文件中读取RSA公钥(Base64编码)
    @Value("${jwt.public-key}")
    private String publicKeyStr;

    // 从配置文件中读取Token过期时间(毫秒)
    @Value("${jwt.expiration}")
    private Long expiration;

    // 从配置文件中读取签发人
    @Value("${jwt.issuer}")
    private String issuer;

    /**
     * 解析RSA私钥(用于生成Token)
     */
    private PrivateKey getPrivateKey() {
        try {
            // 1. 对Base64编码的私钥进行解码
            byte[] privateKeyBytes = Base64.getDecoder().decode(privateKeyStr);
            // 2. 构建PKCS8密钥规范
            PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
            // 3. 生成私钥
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            return keyFactory.generatePrivate(keySpec);
        } catch (Exception e) {
            log.error("RSA私钥解析失败:{}", e.getMessage());
            throw new RuntimeException("私钥解析异常,无法生成Token");
        }
    }

    /**
     * 解析RSA公钥(用于解析、验证Token)
     */
    private PublicKey getPublicKey() {
        try {
            // 1. 对Base64编码的公钥进行解码
            byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyStr);
            // 2. 构建X509密钥规范
            X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes);
            // 3. 生成公钥
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            return keyFactory.generatePublic(keySpec);
        } catch (Exception e) {
            log.error("RSA公钥解析失败:{}", e.getMessage());
            throw new RuntimeException("公钥解析异常,无法验证Token");
        }
    }

    /**
     * 生成JWT Token(使用RS256非对称加密)
     * @param claims 自定义声明(存放用户ID、角色等非敏感信息)
     * @return 完整的JWT Token
     */
    public String generateToken(Map<String, Object> claims) {
        // 1. 获取RSA私钥
        PrivateKey privateKey = getPrivateKey();

        // 2. 构建Token(加密算法改为RS256)
        return Jwts.builder()
                .setClaims(claims) // 设置自定义声明
                .setIssuer(issuer) // 设置签发人
                .setIssuedAt(new Date()) // 设置签发时间
                .setExpiration(new Date(System.currentTimeMillis() + expiration)) // 设置过期时间
                .signWith(privateKey, SignatureAlgorithm.RS256) // 改用RS256非对称加密
                .compact(); // 生成Token
    }

    /**
     * 解析JWT Token,获取自定义声明(使用RSA公钥)
     * @param token JWT Token(不含前缀)
     * @return 自定义声明(Map形式)
     */
    public Map<String, Object> parseToken(String token) {
        try {
            PublicKey publicKey = getPublicKey();
            // 解析Token,获取Claims(包含标准声明和自定义声明)
            Claims claims = Jwts.parserBuilder()
                    .setSigningKey(publicKey) // 设置公钥,用于验证签名
                    .build()
                    .parseClaimsJws(token) // 解析Token
                    .getBody(); // 获取Payload中的Claims
            return claims;
        } catch (Exception e) {
            log.error("JWT Token解析失败:{}", e.getMessage());
            throw new RuntimeException("Token无效或已过期");
        }
    }

    /**
     * 验证JWT Token是否有效(使用RSA公钥)
     * @param token JWT Token(不含前缀)
     * @return true:有效,false:无效
     */
    public boolean validateToken(String token) {
        try {
            PublicKey publicKey = getPublicKey();
            // 解析Token,若解析失败(如篡改、过期),会抛出异常
            Jwts.parserBuilder()
                    .setSigningKey(publicKey)
                    .build()
                    .parseClaimsJws(token);
            return true;
        } catch (ExpiredJwtException e) {
            log.error("JWT Token已过期:{}", e.getMessage());
        } catch (MalformedJwtException e) {
            log.error("JWT Token格式错误:{}", e.getMessage());
        } catch (SignatureException e) {
            log.error("JWT Token签名异常(可能被篡改):{}", e.getMessage());
        } catch (Exception e) {
            log.error("JWT Token验证失败:{}", e.getMessage());
        }
        return false;
    }

    /**
     * 从Token中获取用户ID(自定义方法,根据业务需求调整)
     * @param token JWT Token(不含前缀)
     * @return 用户ID
     */
    public Long getUserIdFromToken(String token) {
        Map<String, Object> claims = parseToken(token);
        // 注意:这里的"userId"要和生成Token时存入的自定义声明key一致
        return Long.parseLong(claims.get("userId").toString());
    }

    /**
     * 从Token中获取用户角色(自定义方法,根据业务需求调整)
     * @param token JWT Token(不含前缀)
     * @return 用户角色
     */
    public String getRoleFromToken(String token) {
        Map<String, Object> claims = parseToken(token);
        return claims.get("role").toString();
    }

    /**
     * 判断Token是否即将过期(自定义阈值,如剩余时间小于300秒,即5分钟)
     * @param token JWT Token(不含前缀)
     * @param threshold 过期阈值(毫秒),如300000毫秒(5分钟)
     * @return true:即将过期,false:未即将过期
     */
    public boolean isTokenAboutToExpire(String token, Long threshold) {
        Map<String, Object> claims = parseToken(token);
        Date expiration = claims.getExpiration(); // 获取Token过期时间
        Date now = new Date();
        // 计算剩余时间 = 过期时间 - 当前时间
        long remainingTime = expiration.getTime() - now.getTime();
        // 若剩余时间小于阈值,说明即将过期
        return remainingTime < threshold;
    }
}
5. 核心修改说明
  • 新增getPrivateKey()和getPublicKey()方法,用于解析配置文件中的Base64编码密钥,转换为JWT所需的PrivateKey和PublicKey;

  • 生成Token时,改用signWith(privateKey, SignatureAlgorithm.RS256),使用私钥进行RS256加密;

  • 解析、验证Token时,改用公钥setSigningKey(publicKey),确保只有持有私钥的服务器能生成有效Token,公钥仅用于校验;

  • 原有其他方法(获取用户ID、角色、判断Token即将过期)保持不变,无需修改Service、Controller、拦截器代码,实现无缝替换。

6. 测试验证(确保非对称加密生效)

沿用原有接口测试流程,无需修改测试步骤,仅需注意:

  1. 运行RsaKeyGenerator生成密钥对,将私钥、公钥正确配置到application.yml;

  2. 启动项目,调用POST /api/login接口,获取RS256加密生成的Token;

  3. 使用该Token访问/api/user/info、/api/admin/operate接口,验证Token可正常解析;

  4. 尝试篡改Token的任意字符,再次访问接口,会返回"Token无效或已过期",说明签名验证生效。

补充说明:HS256与RS256适用场景对比

加密方式 适用场景 优势 劣势
HS256(对称) 单体项目、测试环境、非核心业务场景 简单高效、开发成本低 密钥泄露风险高,安全性低
RS256(非对称) 分布式项目、生产环境、核心业务场景 安全性高,私钥可单独保管 加密解密耗时略高于HS256

总结:生产环境优先使用RS256非对称加密,确保Token安全性;测试环境或简单单体项目可使用HS256,提升开发效率。两种加密方式可通过修改JwtUtil和配置文件无缝切换,无需改动业务代码。

相关推荐
曲幽8 天前
不止于JWT:用FastAPI的Depends实现细粒度权限控制
python·fastapi·web·jwt·rbac·permission·depends·abac
黑白极客14 天前
ACP大模型认证刷题工具开源,助力高效备考
java·ai·github·llama·认证
源代码•宸17 天前
简版抖音项目——项目需求、项目整体设计、Gin 框架使用、视频模块方案设计、用户与鉴权模块方案设计、JWT
经验分享·后端·golang·音视频·gin·jwt·gorm
玄〤17 天前
个人博客网站搭建day3--Spring Boot JWT Token 认证配置的完整实现详解(漫画解析)
java·spring boot·后端·jwt
玄〤19 天前
个人博客网站搭建day2-Spring Boot 3 + JWT + Redis 实现后台权限拦截与单点登录(漫画解析)
java·spring boot·redis·后端·jwt
小钻风336625 天前
JWT初识
java·jwt·base64url
indexsunny1 个月前
互联网大厂Java面试实战:从Spring Boot到微服务架构的技术问答解析
java·spring boot·redis·微服务·kafka·jwt·flyway