引言:入职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点------
-
无状态:服务器无需存储用户信息,只需解密Token即可完成校验,减轻服务器压力,完美适配分布式部署;
-
多端适配:Token可通过请求头、URL、请求体携带,无需依赖Cookie,适配APP、小程序、PC端等多端场景;
-
安全性高:支持多种加密算法(HS256、RS256等),Token具有签名防伪机制,篡改后会直接失效;
-
可扩展性强: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对称加密为例)
-
将Header和Payload分别进行Base64编码,得到编码后的字符串;
-
用英文句号(.)连接编码后的Header和Payload,得到 Header编码.Payload编码;
-
使用提前约定好的密钥(secret),对上述字符串进行HS256加密,得到签名;
-
将签名作为Token的第三部分,与Header编码、Payload编码用句号连接,得到完整的JWT Token。
(2)签名验证逻辑
服务器收到客户端携带的Token后,会执行以下步骤验证Token的有效性:
-
将Token按句号分隔,得到Header编码、Payload编码、Signature;
-
对Header编码、Payload编码进行Base64解码,获取加密算法(alg)和用户信息;
-
使用相同的密钥(secret),对 Header编码.Payload编码进行加密,得到新的签名;
-
将新的签名与客户端携带的Signature进行对比,若一致,说明Token未被篡改;若不一致,说明Token被篡改,直接拒绝请求;
-
验证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. 测试验证(确保非对称加密生效)
沿用原有接口测试流程,无需修改测试步骤,仅需注意:
-
运行RsaKeyGenerator生成密钥对,将私钥、公钥正确配置到application.yml;
-
启动项目,调用POST /api/login接口,获取RS256加密生成的Token;
-
使用该Token访问/api/user/info、/api/admin/operate接口,验证Token可正常解析;
-
尝试篡改Token的任意字符,再次访问接口,会返回"Token无效或已过期",说明签名验证生效。
补充说明:HS256与RS256适用场景对比
| 加密方式 | 适用场景 | 优势 | 劣势 |
|---|---|---|---|
| HS256(对称) | 单体项目、测试环境、非核心业务场景 | 简单高效、开发成本低 | 密钥泄露风险高,安全性低 |
| RS256(非对称) | 分布式项目、生产环境、核心业务场景 | 安全性高,私钥可单独保管 | 加密解密耗时略高于HS256 |
总结:生产环境优先使用RS256非对称加密,确保Token安全性;测试环境或简单单体项目可使用HS256,提升开发效率。两种加密方式可通过修改JwtUtil和配置文件无缝切换,无需改动业务代码。