第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_id、department |
举个例子:
{
"sub": "user_12345",
"name": "张三",
"role": "admin",
"iat": 1516239022,
"exp": 1516242622
}
这里要注意——Payload只是Base64编码,不是加密。我在项目中见过有人把密码直接塞进Payload里,这简直是给黑客送人头。你想想看,任何拿到JWT的人都能解码看到里面的内容。
Signature(签名)
签名是JWT的「防伪标识」。它的生成过程是这样的:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
说白了,就是把头部和载荷拼起来,再用密钥算个哈希。如果有人篡改了Payload,签名验证就会失败。我曾经在项目中遇到过有人试图修改JWT里的角色信息,结果被签名验证直接拦下来了。
3.2 签发与验证流程
整个流程其实就两步:签发和验证。我画个流程图给你看:
签发流程
- 用户登录,提交用户名和密码
- 服务端验证身份,确认无误
- 构建Header和Payload
- 用密钥生成签名
- 把三部分拼起来,返回给客户端
代码实现大概是这样的:
// 签发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));
}
验证流程
- 客户端携带JWT访问资源
- 服务端解析JWT,分离出Header、Payload、Signature
- 用同样的密钥重新计算签名
- 比对两个签名是否一致
- 检查Payload中的
exp是否过期 - 验证通过,提取用户信息
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不同,有效期更短,权限更严格。
场景三:单点登录(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刷新等。到时候我会分享更多实战中的坑和技巧。