JWT Token 生成/解析/校验工具类

概述

在 Spring Boot 项目中实现基于 JWT 的认证机制时,核心工作集中在 Token 的生成、解析和校验三个环节。本文介绍一个基于 JJWT 0.12.x 的工具类实现,以及对应的 YAML 配置方式。

依赖引入

//jwt(jjwt 0.12.x)
    implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
    runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
    runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'

JJWT 从 0.12.x 开始将 API 与实现分离,jjwt-jackson 用于 JSON 序列化/反序列化的支持,runtimeOnly 作用域即可。

工具类实现

完整代码

package com.example.project.util;

import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
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;

/**
 * JWT工具类(使用jjwt 0.12.x API)
 */
@Component
public class JwtUtil {

    private final SecretKey secretKey;
    private final long expirationMs;

    public JwtUtil(@Value("${jwt.secret}") String secret,
                   @Value("${jwt.expiration-ms}") long expirationMs) {
        this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
        this.expirationMs = expirationMs;
    }

    /**
     * 生成Token
     * @param userId 用户ID,作为subject存储
     * @param username 用户名,作为自定义claim存储
     * @return 签发后的JWT字符串
     */
    public String generateToken(Long userId, String username) {
        Date now = new Date();
        return Jwts.builder()
                .subject(String.valueOf(userId))
                .claim("username", username)
                .issuedAt(now)
                .expiration(new Date(now.getTime() + expirationMs))
                .signWith(secretKey)
                .compact();
    }

    /**
     * 从Token中解析用户ID
     * @param token JWT字符串
     * @return 用户ID
     */
    public Long getUserId(String token) {
        return Long.parseLong(
                Jwts.parser().verifyWith(secretKey).build()
                        .parseSignedClaims(token).getPayload().getSubject()
        );
    }

    /**
     * 从Token中解析用户名
     * @param token JWT字符串
     * @return 用户名
     */
    public String getUsername(String token) {
        return Jwts.parser().verifyWith(secretKey).build()
                .parseSignedClaims(token).getPayload().get("username", String.class);
    }

    /**
     * 校验Token是否合法
     * @param token JWT字符串
     * @return true-合法 / false-非法或已过期
     */
    public boolean validateToken(String token) {
        try {
            Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }

    public long getExpirationMs() {
        return expirationMs;
    }
}

关键设计点

1. 密钥处理

HMAC-SHA 算法要求密钥长度至少为 256 位(32 字节)。代码中通过 Keys.hmacShaKeyFor() 将配置文件中的字符串转换为 SecretKey 对象:

this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));

这里直接指定 StandardCharsets.UTF_8,避免依赖系统默认编码,确保跨环境一致性。

2. Token 结构

生成的 Token 包含以下字段:

字段 存储位置 说明
userId subject (sub) JWT 标准声明,存放用户唯一标识
username 自定义 claim 额外携带的用户信息
issuedAt 标准声明 (iat) Token 签发时间
expiration 标准声明 (exp) Token 过期时间

将 userId 放入 subject 而非自定义 claim,是遵循 JWT 规范的常见做法——subject 字段的语义就是标识 Token 的主体身份。

3. 解析与校验的分离

代码中 validateTokengetUserId/getUsername 在功能上有重叠:解析方法本身就会执行签名验证和过期检查。这种设计的考虑在于:

  • 校验接口用于过滤器链中的快速判断,只需知道合法与否
  • 解析接口用于业务层提取身份信息,同时完成合法性验证

实际调用时无需先校验再解析,直接调用解析方法即可获得相同的安全保障。

4. 异常处理

validateToken 方法捕获两类异常:

  • JwtException:签名无效、格式错误、Token 过期等 JWT 相关异常
  • IllegalArgumentException:传入 null 或空字符串时抛出

返回布尔值而非抛异常,是为了让上层调用方(如 Filter)能根据返回值灵活决定响应策略。

配置文件

application.yml 中添加以下配置:

spring:
  application:
    name: reference-self
  profiles:
    active: dev

# ---- JWT 配置 ----
jwt:
  secret: "SpringBootTutorialSecretKey_MinLength_32chars_OK"
  expiration-ms: 86400000   # 24小时,单位:毫秒

几点说明:

  • secret:HMAC 密钥,生产环境应从环境变量或密钥管理服务读取,不应明文写在配置文件中。最小长度 32 字符(256 位)
  • expiration-ms:Token 有效期,示例值为 24 小时(86400000 毫秒)。根据业务场景可调整为更短的时间以增强安全性

使用示例

登录签发 Token

@Service
@RequiredArgsConstructor
public class AuthService {

    private final JwtUtil jwtUtil;

    public String login(LoginRequest request) {
        // 1. 校验用户名密码(省略)
        // 2. 生成Token并返回
        return jwtUtil.generateToken(user.getId(), user.getUsername());
    }
}

请求拦截校验

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) {
        String token = extractToken(request); // 从Header中提取

        if (token != null && jwtUtil.validateToken(token)) {
            Long userId = jwtUtil.getUserId(token);
            // 设置安全上下文(省略)
        }
        chain.doFilter(request, response);
    }
}

业务层获取用户信息

@GetMapping("/profile")
public ResponseEntity<UserProfile> getProfile(HttpServletRequest request) {
    String token = extractToken(request);
    Long userId = jwtUtil.getUserId(token);
    String username = jwtUtil.getUsername(token);
    // 查询并返回用户信息
}

常见问题

Token 过期后如何处理?

当前实现为无状态 Token,过期后只能重新登录签发。如果需要续期能力,通常有两种方案:

  1. 缩短有效期 + Refresh Token:Access Token 有效期设为 15-30 分钟,配合较长期的 Refresh Token 实现无感刷新
  2. 滑动窗口:每次请求成功后重新签发新 Token,前端替换本地存储的旧 Token

生产环境的密钥管理?

开发阶段可以硬编码在 yml 中,但生产环境建议通过以下任一方式注入:

# 方式一:环境变量
export JWT_SECRET="your-production-secret-key-at-least-32-chars"

# 方式二:启动参数
java -jar app.jar --jwt.secret="${JWT_SECRET}"

yml 中引用:

jwt:
  secret: ${JWT_SECRET:default-fallback-key}
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐