Spring Boot JWT Token 认证完整实现详解
Spring Boot 3 + JWT + Redis 实现后台权限拦截与单点登录的内容:Spring Boot 3 + JWT + Redis 实现后台权限拦截与单点登录
前言
在网站的开发中,基于 Token 的无状态认证已经成为主流方案。其中 JWT(JSON Web Token)因其自包含、易扩展和跨域友好等特性,被广泛应用于前后端分离架构。

由于本人的个人博客网站决定采取前后端的形式搭建,于是使用了JWT Token的这种形式去实现认证,接下来为其中的一些要点
一、JwtUtils 工具类:Token 全生命周期管理
1. 配置注入
java
@Component
@Getter
public class JwtUtils {
private final String secretKey;
private final long expiration;
private final String header;
private final String tokenPrefix;
public JwtUtils(@Value("${jwt.secret}") String secret,
@Value("${jwt.expiration}") long expiration,
@Value("${jwt.header}") String header,
@Value("${jwt.token-prefix}") String tokenPrefix) {
this.secretKey = secret;
this.expiration = expiration;
this.header = header;
this.tokenPrefix = tokenPrefix;
}
}
通过@Value注解可以从 application.yaml 读取密钥、过期时间、Header 名和前缀,配置与代码完全解耦。
jwt.secret:签名密钥(用于 HS256 对称加密)jwt.expiration:Token 有效期(单位秒)jwt.header:请求头名称(默认Authorization)jwt.token-prefix:Token 前缀(默认Bearer)
yaml
# JWT 配置
jwt:
secret: opusnocturne_jwt_secret_key_2026
expiration: 86400 # 过期时间(秒),默认1天
header: Authorization # 请求头名称
token-prefix: Bearer # token前缀

2. Token 生成
java
public String generateToken(String username, Map<String, Object> claims) {
try {
JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder();
if (claims != null && !claims.isEmpty()) {
for (Map.Entry<String, Object> entry : claims.entrySet()) {
builder.claim(entry.getKey(), entry.getValue());
}
}
builder.subject(username);
Date expirationTime = new Date(System.currentTimeMillis() + expiration * 1000);
builder.expirationTime(expirationTime);
builder.issueTime(new Date());
JWTClaimsSet claimsSet = builder.build();
JWSHeader jwsHeader = new JWSHeader(JWSAlgorithm.HS256);
JWSObject jwsObject = new JWSObject(jwsHeader, new Payload(claimsSet.toJSONObject()));
jwsObject.sign(new MACSigner(secretKey.getBytes(StandardCharsets.UTF_8)));
return jwsObject.serialize();
} catch (Exception e) {
log.error("生成Token失败: {}", e.getMessage());
throw new RuntimeException("Failed to generate token", e);
}
}
支持自定义 Claims(角色、权限等),使用 Nimbus JOSE+JWT 库 + HS256 对称签名。
generateToken(String username, Map<String, Object> claims)方法是生成Token 的核心:
- 使用
JWTClaimsSet.Builder构建声明集- 添加自定义 Claims(如角色、用户 ID 等)
- 设置
subject(通常为用户名或用户唯一标识)- 记录
issueTime(签发时间)和expirationTime(过期时间)- 使用 Nimbus JOSE+JWT 库的
JWSHeader指定HS256算法- 通过
MACSigner对密钥进行签名,最后序列化得到字符串 Token

3. Token 解析与校验
java
public JWTClaimsSet getAllClaimsFromToken(String token) {
try {
SignedJWT signedJWT = SignedJWT.parse(token);
JWSVerifier verifier = new MACVerifier(secretKey.getBytes(StandardCharsets.UTF_8));
if (!signedJWT.verify(verifier)) {
throw new Exception("Invalid JWT signature");
}
return signedJWT.getJWTClaimsSet();
} catch (Exception e) {
log.error("解析Token失败: {}", e.getMessage());
throw new RuntimeException("Invalid Token");
}
}
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, JWTClaimsSet::getSubject);
}
java
public <T> T getClaimFromToken(String token, Function<JWTClaimsSet, T> claimsResolver) {
final JWTClaimsSet claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
核心方法 getAllClaimsFromToken 负责签名验证,任何异常都会被捕获并抛出,便于上层拦截器统一处理。
解析部分是安全性的关键:
getAllClaimsFromToken(String token):先用SignedJWT.parse()解析,再用MACVerifier校验签名- 签名不通过或格式错误时直接抛出异常,避免后续业务执行
- 提供泛型方法
getClaimFromToken提取任意声明(如getUsernameFromToken、getExpirationDateFromToken)validateToken方法同时校验用户名一致性和未过期状态

二、JwtInterceptor 拦截器:请求级认证与状态检查
1. preHandle 核心逻辑
java
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
return true;
}
String token = request.getHeader(jwtUtils.getHeader());
if (token == null || token.isEmpty()) {
writeErrorResponse(response, ErrorCode.UNAUTHORIZED);
return false;
}
String tokenPrefix = jwtUtils.getTokenPrefix();
if (token.startsWith(tokenPrefix)) {
token = token.substring(tokenPrefix.length()).trim();
}
try {
JWTClaimsSet claimsSet = jwtUtils.getAllClaimsFromToken(token);
String username = claimsSet.getSubject();
// Redis 状态检查
String redisKey = RedisConstant.TOKEN_KEY_PREFIX + username;
String storedToken = stringRedisTemplate.opsForValue().get(redisKey);
if (storedToken == null) {
writeErrorResponse(response, ErrorCode.TOKEN_EXPIRED);
return false;
}
if (!storedToken.equals(token)) {
writeErrorResponse(response, ErrorCode.TOKEN_REPLACED);
return false;
}
// 存入 request 属性,供后续使用
request.setAttribute("userClaims", claimsSet);
request.setAttribute("username", username);
request.setAttribute("userId", claimsSet.getClaim("userId"));
return true;
} catch (Exception e) {
log.error("Token 验证失败: {}", e.getMessage());
writeErrorResponse(response, ErrorCode.TOKEN_INVALID);
return false;
}
}

2. HandlerInterceptor 的正确使用
JwtInterceptor 实现了 Spring MVC 的 HandlerInterceptor 接口,核心逻辑放在 preHandle 方法中:
- 首先判断
OPTIONS请求(CORS 预检)直接放行,避免跨域问题 - 从指定 Header 中获取 Token
- 自动移除前缀(如
Bearer),提升兼容性

3. Redis 辅助的 Token 生命周期管理
这是该实现区别于普通 JWT 的亮点:
- 生成 Token 后,后端同时把 Token 存入 Redis,Key 为
TOKEN_KEY_PREFIX + username - 拦截器中检查 Redis 是否存在该 Token:
- 不存在 → 已登出或被挤掉
- 存在但与当前 Token 不一致 → 用户在其他设备重新登录(实现单设备登录)
- 登出时只需删除 Redis 中的 Token 即可实现即时失效,解决了 JWT 无法主动撤销的问题

4. 用户信息传递机制
验证通过后,将解析出的 JWTClaimsSet 和常用字段(username、userId)通过 request.setAttribute 存入请求属性,后续 Controller 或 Service 可直接从 request 中获取,无需重复解析 Token。
5. 统一错误响应
java
private void writeErrorResponse(HttpServletResponse response, ErrorCode errorCode) throws Exception {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_OK); // HTTP 状态码永远 200
Result<?> result = Result.error(errorCode);
response.getWriter().write(JSON.toJSONString(result));
}
所有认证失败场景(无 Token、Token 无效、过期、被替换等)均调用
writeErrorResponse:
- HTTP 状态码统一返回 200(符合 RESTful 规范,避免浏览器拦截非 2xx 响应)
- 返回体为统一的
ResultJSON 对象,内部通过业务错误码(如UNAUTHORIZED、TOKEN_EXPIRED、TOKEN_REPLACED、TOKEN_INVALID)区分具体原因- 前端根据业务码做对应处理(如 2001 跳转登录页)
这种设计极大简化了前端错误处理逻辑,是企业级项目常见的实践

三、整体架构与安全亮点
- Redis 状态管理 :登录时
stringRedisTemplate.opsForValue().set(redisKey, token, expiration, TimeUnit.SECONDS);登出时delete(redisKey),完美解决 JWT 无法主动失效的问题。 - 用户信息传递 :
request.setAttribute方式无需每次解析 Token,后续 Controller 可直接String username = (String) request.getAttribute("username"); - 异常与日志:所有关键路径均有 SLF4J 日志记录,生产环境可快速定位。
- Lombok + RequiredArgsConstructor:注入 JwtUtils 和 StringRedisTemplate 简洁优雅。
其中采取了对称加密 HS256 :速度快,适合内部系统;生产环境建议配合密钥轮换机制。
以及使用了Nimbus JOSE+JWT 库:相比 jjwt 等其他库,Nimbus 对 RFC 规范支持更完整,推荐用于对标准要求严格的项目。

四、典型使用示例
登录 Controller 片段:
java
@PostMapping("/login")
public Result<String> login(@RequestBody LoginDTO dto) {
// ... 密码校验通过
String token = jwtUtils.generateToken(username, Map.of("userId", userId, "roles", roles));
stringRedisTemplate.opsForValue().set(RedisConstant.TOKEN_KEY_PREFIX + username, token, jwtUtils.getExpiration(), TimeUnit.SECONDS);
return Result.success(token);
}
登出:
java
@PostMapping("/logout")
public Result<Void> logout(HttpServletRequest request) {
String username = (String) request.getAttribute("username");
stringRedisTemplate.delete(RedisConstant.TOKEN_KEY_PREFIX + username);
return Result.success();
}

五、总结
通过 JwtUtils 完成 Token 的生成、解析与基础验证,配合 JwtInterceptor + Redis 实现完整的认证闭环,该方案兼具高安全性、可扩展性和生产可用性。
