欢迎光临
我们一直在努力

个人博客网站搭建day3--Spring Boot JWT Token 认证配置的完整实现详解(漫画解析)

Spring Boot JWT Token 认证完整实现详解

Spring Boot 3 + JWT + Redis 实现后台权限拦截与单点登录的内容:Spring Boot 3 + JWT + Redis 实现后台权限拦截与单点登录

前言

在网站的开发中,基于 Token 的无状态认证已经成为主流方案。其中 JWT(JSON Web Token)因其自包含、易扩展和跨域友好等特性,被广泛应用于前后端分离架构。 在这里插入图片描述

由于本人的个人博客网站决定采取前后端的形式搭建,于是使用了JWT Token的这种形式去实现认证,接下来为其中的一些要点


一、JwtUtils 工具类:Token 全生命周期管理

1. 配置注入

@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 )

# JWT 配置
jwt:
secret: opusnocturne_jwt_secret_key_2026
expiration: 86400 # 过期时间(秒),默认1天
header: Authorization # 请求头名称
token-prefix: Bearer # token前缀

在这里插入图片描述


2. Token 生成

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 解析与校验

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);
}

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 核心逻辑

@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. 统一错误响应

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 响应)
  • 返回体为统一的 Result JSON 对象,内部通过业务错误码(如 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 片段:

    @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);
    }

    登出:

    @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 实现完整的认证闭环,该方案兼具高安全性、可扩展性和生产可用性。 在这里插入图片描述

    赞(0)
    未经允许不得转载:171主机测评 » 个人博客网站搭建day3--Spring Boot JWT Token 认证配置的完整实现详解(漫画解析)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址