4、JWT实战:从结构到安全,一次讲透

JWT 这东西,说白了就是一张“数字化通行证”。你在后端签发给客户端,客户端每次请求时带上它,你就能认出是谁。我最早接触 JWT 是在做一个移动端登录改造项目,那时候踩了不少坑,今天我把这些经验都揉碎了讲给你。

4.1 JWT 长什么样?拆开看看

一个 JWT 令牌,看起来就是一长串乱码,用两个点分成三段:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

三段分别是:Header(头部)、Payload(载荷)、Signature(签名)。每一段都是 Base64URL 编码后的 JSON。

部分 内容 是否加密
Header 算法类型(如 HS256、RS256)、令牌类型 仅编码,不加密
Payload 用户标识、过期时间、自定义声明等 仅编码,不加密
Signature 对前两段的签名,防止篡改 由密钥生成
⚠️ 重要提醒:Payload 只是 Base64 编码,不是加密!任何人都可以解码看到里面的内容。我在项目中见过有人把密码直接放进去,这是非常危险的做法。

4.2 生成与验证:手写一个 JWT 工具

我个人习惯用 jjwt 这个库,它把底层细节封装得很好。下面是一个完整的生成和验证示例:

// 生成 JWT
String secretKey = "my-secret-key-at-least-256-bits-long";
String token = Jwts.builder()
    .setSubject("user-001")
    .claim("role", "admin")
    .setIssuedAt(new Date())
    .setExpiration(new Date(System.currentTimeMillis() + 3600_000)) // 1小时过期
    .signWith(SignatureAlgorithm.HS256, secretKey)
    .compact();

// 验证 JWT
try {
    Claims claims = Jwts.parser()
        .setSigningKey(secretKey)
        .parseClaimsJws(token)
        .getBody();
    System.out.println("用户: " + claims.getSubject());
    System.out.println("角色: " + claims.get("role"));
} catch (ExpiredJwtException e) {
    System.out.println("令牌已过期");
} catch (SignatureException e) {
    System.out.println("签名无效,令牌被篡改");
}

这里有几个关键点:

  • 密钥长度:HS256 要求密钥至少 256 位,太短会被暴力破解
  • 过期时间:一定要设,不设等于给攻击者留后门
  • 异常处理:过期和签名异常要分开处理,不能笼统地返回“无效令牌”
💡 我的经验:生产环境中,我建议把密钥放在配置中心或环境变量里,不要硬编码在代码中。曾经有个项目因为密钥写死在代码里,上线后想换密钥,结果要重新部署所有服务,非常被动。

4.3 Refresh Token 机制:让用户少登录

Access Token 过期时间短(比如 15 分钟),是为了安全。但用户不可能每 15 分钟登录一次,所以需要 Refresh Token。

流程是这样的:

  1. 用户登录后,服务端返回两个令牌:Access Token(短期)和 Refresh Token(长期,比如 7 天)
  2. Access Token 过期后,客户端用 Refresh Token 去换新的 Access Token
  3. 服务端验证 Refresh Token 有效,签发新的 Access Token
  4. Refresh Token 也可以轮换,每次使用后发一个新的
// 生成 Refresh Token
String refreshToken = Jwts.builder()
    .setSubject("user-001")
    .setId(UUID.randomUUID().toString()) // 唯一标识,用于撤销
    .setExpiration(new Date(System.currentTimeMillis() + 7 * 24 * 3600_000)) // 7天
    .signWith(SignatureAlgorithm.HS256, refreshSecretKey)
    .compact();

// 刷新 Access Token
public String refreshAccessToken(String refreshToken) {
    try {
        Claims claims = Jwts.parser()
            .setSigningKey(refreshSecretKey)
            .parseClaimsJws(refreshToken)
            .getBody();
        // 检查该 refreshToken 是否已被撤销(查数据库或 Redis)
        if (isRevoked(claims.getId())) {
            throw new RuntimeException("Refresh Token 已被撤销");
        }
        // 签发新的 Access Token
        return Jwts.builder()
            .setSubject(claims.getSubject())
            .setExpiration(new Date(System.currentTimeMillis() + 900_000)) // 15分钟
            .signWith(SignatureAlgorithm.HS256, accessSecretKey)
            .compact();
    } catch (ExpiredJwtException e) {
        throw new RuntimeException("Refresh Token 已过期,请重新登录");
    }
}
🔑 核心原则:Refresh Token 必须可以撤销。我曾经遇到一个案例,用户的 Refresh Token 被盗,攻击者持续刷新 Access Token 长达一周。后来我们在 Redis 里维护了一个撤销列表,每次刷新时检查,才堵住这个漏洞。

4.4 常见安全问题:这些坑我都踩过

JWT 虽然方便,但用不好就是灾难。下面是我总结的几个高频问题:

问题 表现 解决方案
密钥泄露 攻击者可伪造任意令牌 定期轮换密钥,使用 RS256 非对称算法
算法混淆 攻击者将 RS256 改为 HS256,用公钥签名 服务端强制指定算法,拒绝算法为 "none" 的令牌
令牌未过期 令牌被窃取后长期有效 设置短过期时间 + Refresh Token 机制
Payload 泄露敏感信息 Base64 解码即可看到用户手机号、密码 Payload 只放非敏感信息,必要时对 Payload 加密
无状态导致无法撤销 令牌签发后无法主动失效 维护黑名单(Redis),或使用短令牌 + 刷新机制
⚠️ 算法混淆攻击:这是我在一次安全审计中发现的。攻击者把 JWT 的 Header 里的 alg 从 RS256 改成 HS256,然后用公钥(通常是公开的)作为密钥去签名。如果服务端没有校验算法类型,就会认为这个令牌是合法的。解决方案很简单:在解析时明确指定算法。

4.5 核心流程一览

下面这张图展示了 JWT 从签发到验证的完整流程,以及 Refresh Token 的交互过程:

JWT 签发与验证流程 客户端 认证服务 业务服务 1. 登录请求 2. 返回 Access + Refresh Token 3. 携带 Access Token 请求资源 验证签名 + 过期时间 4. 返回数据或 401 Access Token 过期 5. 发送 Refresh Token 验证 Refresh Token 6. 返回新的 Access Token 关键说明: • Access Token 过期时间短(15-30分钟),减少泄露风险 • Refresh Token 过期时间长(7-30天),但必须可撤销 • 业务服务只验证 Access Token,不存储会话状态 • 认证服务维护 Refresh Token 的黑名单或版本号
💡 我的建议:如果你刚开始用 JWT,先从 HS256 对称算法入手,理解流程后再切换到 RS256 非对称算法。非对称算法的好处是,业务服务只需要持有公钥,即使被攻破也无法签发令牌。我在一个金融项目中就强制要求使用 RS256,因为安全审计不允许对称密钥在多服务间共享。

嗯,JWT 这块内容就这些。记住一句话:JWT 不是银弹,它解决了无状态认证的问题,但也带来了令牌撤销、密钥管理等新挑战。理解它的原理和边界,你才能用好它。