第3章:JWT原理与实战

JWT这东西,说白了就是微服务之间传递身份信息的一张「通行证」。我刚开始接触微服务时,总觉得Session共享也能凑合用,直到有一次线上出了跨域认证问题……嗯,从那以后我就彻底转向JWT了。

3.1 JWT的结构:三个部分,各司其职

一个JWT长什么样?我给你看个例子:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

看到没?三个部分用点号隔开。我习惯把它们叫做:头、身子、签名。

Header(头部)

头部就是个JSON对象,通常长这样:

{
  "alg": "HS256",
  "typ": "JWT"
}

alg字段告诉系统:「我是用HS256算法签名的」。typ字段就是个标识,告诉你这是个JWT。我个人习惯在头部里再加个kid字段,用来标识用的是哪把密钥——这在多密钥轮换的场景下特别有用。

Payload(载荷)

载荷才是JWT的核心,它里面放着真正的数据。我把它分成三类:

类型 说明 示例
注册声明 预定义的标准化字段 iss(签发者)、exp(过期时间)、sub(主题)
公共声明 自定义字段,需避免冲突 role(角色)、scope(权限范围)
私有声明 双方约定的业务数据 user_iddepartment

举个例子:

{
  "sub": "user_12345",
  "name": "张三",
  "role": "admin",
  "iat": 1516239022,
  "exp": 1516242622
}

这里要注意——Payload只是Base64编码,不是加密。我在项目中见过有人把密码直接塞进Payload里,这简直是给黑客送人头。你想想看,任何拿到JWT的人都能解码看到里面的内容。

⚠️ 重要提醒: 永远不要在Payload中存放敏感信息,比如密码、信用卡号。Base64编码不是加密,它只是把二进制数据转成可读文本而已。

Signature(签名)

签名是JWT的「防伪标识」。它的生成过程是这样的:

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

说白了,就是把头部和载荷拼起来,再用密钥算个哈希。如果有人篡改了Payload,签名验证就会失败。我曾经在项目中遇到过有人试图修改JWT里的角色信息,结果被签名验证直接拦下来了。

3.2 签发与验证流程

整个流程其实就两步:签发和验证。我画个流程图给你看:

签发流程

  1. 用户登录,提交用户名和密码
  2. 服务端验证身份,确认无误
  3. 构建Header和Payload
  4. 用密钥生成签名
  5. 把三部分拼起来,返回给客户端

代码实现大概是这样的:

// 签发JWT
String createJWT(String userId, String role, String secret) {
    // 1. 构建Header
    Map<String, Object> header = new HashMap<>();
    header.put("alg", "HS256");
    header.put("typ", "JWT");

    // 2. 构建Payload
    Map<String, Object> payload = new HashMap<>();
    payload.put("sub", userId);
    payload.put("role", role);
    payload.put("iat", System.currentTimeMillis() / 1000);
    payload.put("exp", System.currentTimeMillis() / 1000 + 3600); // 1小时过期

    // 3. 生成签名并拼接
    return JWT.create()
        .withHeader(header)
        .withPayload(payload)
        .sign(Algorithm.HMAC256(secret));
}

验证流程

  1. 客户端携带JWT访问资源
  2. 服务端解析JWT,分离出Header、Payload、Signature
  3. 用同样的密钥重新计算签名
  4. 比对两个签名是否一致
  5. 检查Payload中的exp是否过期
  6. 验证通过,提取用户信息
💡 关键点: 验证签名时,一定要用服务端保存的密钥重新计算,而不是直接解码。签名验证失败,直接拒绝请求,不要给任何提示信息。

3.3 在微服务中的使用场景

微服务架构下,JWT简直是认证的「标配」。我总结了几种常见场景:

场景一:API网关统一认证

这是最常见的用法。客户端登录后拿到JWT,后续所有请求都带上它。API网关负责验证JWT,验证通过后把用户信息传给下游服务。

// API网关验证JWT
public boolean validateToken(String token, String secret) {
    try {
        DecodedJWT jwt = JWT.require(Algorithm.HMAC256(secret))
            .build()
            .verify(token);
        // 验证通过,提取用户信息
        String userId = jwt.getSubject();
        String role = jwt.getClaim("role").asString();
        // 把用户信息放入请求上下文
        SecurityContext.setCurrentUser(userId, role);
        return true;
    } catch (JWTVerificationException e) {
        // 验证失败,记录日志
        log.warn("JWT验证失败: {}", e.getMessage());
        return false;
    }
}

场景二:服务间调用

服务A调用服务B时,也需要传递身份信息。这时候JWT就派上用场了。我习惯在服务间调用时使用「服务间JWT」,它和用户JWT不同,有效期更短,权限更严格。

🔧 实战技巧: 服务间JWT的有效期建议设为5分钟以内。我见过有人设成24小时,结果密钥泄露后,攻击者拿着JWT到处调用服务,那场面……嗯,你懂的。

场景三:单点登录(SSO)

多个微服务共用一个认证中心。用户在认证中心登录后,拿到JWT,然后拿着它访问各个微服务。每个微服务只需要验证JWT,不需要再问认证中心「这人是谁」。这大大减少了服务间的耦合。

3.4 避坑指南

做JWT这几年,我踩过不少坑。分享几个最痛的:

  • 密钥管理:密钥不要硬编码在代码里。我曾经把密钥写在配置文件里,结果代码泄露到GitHub上……从那以后,我都是用密钥管理服务(如Vault)来管理。
  • 过期时间:不要设得太长。我建议Access Token 15分钟,Refresh Token 7天。太长的话,一旦泄露,攻击者有充足的时间搞破坏。
  • JWT黑名单:JWT一旦签发,在过期前是无法撤销的。如果需要提前失效,就得维护一个黑名单。我一般用Redis来存黑名单,每次验证时查一下。
  • 算法混淆攻击:攻击者可能把alg改成none,绕过签名验证。所以验证时一定要指定算法,不要信任JWT头部里的alg字段。
⚠️ 高危操作: 永远不要使用alg: none的JWT。有些库默认允许无签名JWT,这等于把大门敞开。我建议在验证时强制指定算法,比如Algorithm.HMAC256(secret)

好了,JWT的原理和实战就讲到这里。下一章我们会深入聊聊如何在Spring Cloud微服务中落地JWT认证,包括如何集成Spring Security、如何处理Token刷新等。到时候我会分享更多实战中的坑和技巧。