个人博客网站搭建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. 配置注入

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 提取任意声明(如 getUsernameFromTokengetExpirationDateFromToken
  • 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 响应)
  • 返回体为统一的 Result JSON 对象,内部通过业务错误码(如 UNAUTHORIZEDTOKEN_EXPIREDTOKEN_REPLACEDTOKEN_INVALID)区分具体原因
  • 前端根据业务码做对应处理(如 2001 跳转登录页)

这种设计极大简化了前端错误处理逻辑,是企业级项目常见的实践


三、整体架构与安全亮点

  1. Redis 状态管理 :登录时 stringRedisTemplate.opsForValue().set(redisKey, token, expiration, TimeUnit.SECONDS);登出时 delete(redisKey),完美解决 JWT 无法主动失效的问题。
  2. 用户信息传递request.setAttribute 方式无需每次解析 Token,后续 Controller 可直接 String username = (String) request.getAttribute("username");
  3. 异常与日志:所有关键路径均有 SLF4J 日志记录,生产环境可快速定位。
  4. 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 实现完整的认证闭环,该方案兼具高安全性、可扩展性和生产可用性。

相关推荐
钟智强1 小时前
深度剖析CVE-2023-41064与CVE-2023-4863:libwebp堆溢出漏洞的技术解剖与PoC构建实录
前端·后端
钟智强1 小时前
MySQL客户端惊现高危漏洞CVE-2023-21980,可导致远程代码执行
前端·后端
用户3521802454751 小时前
RAG 做不好?可能是你的 PDF 在"捣乱" 😅
后端·python·ai编程
Cache技术分享1 小时前
332. Java Stream API - Java Stream 实战进阶:按年份找出合作最多的作者对
前端·后端
果冻虾仁1 小时前
vllm使用plugin集成外部模型结构
人工智能·后端
Penge6661 小时前
Go—临时对象池 sync.Pool
后端
hash__1 小时前
AI Agent 技术解析:从原理到实战
后端
马猴烧酒.2 小时前
【JAVA算法|hot100】堆类型题目详解笔记
java·开发语言·笔记
Drifter_yh2 小时前
「JVM」Java 垃圾回收机制全解析:回收算法、分代流转与 G1 收集器底层拆解
java·jvm·算法