JWT 与 JWKS:让服务端离线验证令牌
怎么做到「签发令牌的一方不在线,验证方仍然能确认令牌是真的」? 这篇笔记整理 JWT 的结构、非对称签名的价值,以及 JWKS 公钥分发机制。
从有状态会话说起
最传统的登录方案是服务端会话:用户登录成功后,服务端生成一个随机 session_id,
存进内存或 Redis,再把 session_id 发给客户端。之后每个请求带着它,
服务端查一下存储就知道是谁。
这个方案很可靠,但在两种场景下会变得别扭:
- 验证方不止一个,且彼此不共享存储。 每个验证方都得能访问同一份会话数据,否则验不了。
- 验证方可能连不上网。 如果验证方本身就在一个不稳定的网络环境里,每次验证都去查中心存储是不现实的。
JWT 的思路是把状态放进令牌本身,让验证方不需要查询任何外部存储就能完成验证。
JWT 的结构
一个 JWT 是三段 Base64URL 字符串,用点号连接:
eyJhbGciOiJSUzI1NiIsImtpZCI6IjIwMjYtMDkifQ.eyJzdWIiOiJ1c2VyXzEyMyIsImF1ZCI6Im5hc18xIn0.signature
| 部分 | 内容 |
|---|---|
| Header | 元数据:alg 签名算法、typ 类型、kid 密钥标识 |
| Payload | Claims,即实际的声明内容 |
| Signature | 对前两段的签名 |
Payload 里的标准 claim:
| Claim | 含义 |
|---|---|
iss | 签发者 |
sub | 主体,通常是谁的身份 |
aud | 受众,这个令牌是发给谁的 |
exp | 过期时间 |
iat | 签发时间 |
nbf | 生效时间(在此之前无效) |
jti | 令牌唯一 ID,用于吊销黑名单 |
注意 Payload 只是 Base64URL 编码,不是加密。 任何人都能解开看内容。所以令牌里绝不能放敏感信息 —— 它保证的是「没被篡改」,不是「别人看不见」。
签名:对称与非对称的取舍
HS256 用同一把密钥签名和验证。简单,但有一个致命问题:
- 每一个验证方都必须持有这把密钥
- 持有关键密钥的验证方,也就获得了伪造令牌的能力
- 任何一个验证方泄露,等于全部沦陷,且无法区分是谁泄露的
RS256 / ES256 用的是非对称密钥:签发方持私钥,验证方只需要公钥。 公钥泄露无所谓,因为它只能验证、不能签发。这意味着:
- 验证方可以被信任到「能验证」的程度,而不必被信任到「能签发」的程度
- 公钥可以公开发布,谁都能拿
- 私钥只有签发方一处,泄露面大幅收窄
JWKS:公钥怎么交到验证方手里
公钥既然可以公开,那就可以像普通资源一样通过 HTTP 分发。 JWKS(JSON Web Key Set)就是这个分发的标准格式 —— 一个 JSON 文档,里面是一组 JWK:
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"kid": "2026-09",
"alg": "RS256",
"n": "0vx7agoebGcQ...",
"e": "AQAB"
},
{
"kty": "RSA",
"use": "sig",
"kid": "2026-06",
"alg": "RS256",
"n": "sXchDaQe...",
"e": "AQAB"
}
]
}
约定俗成的端点是 /.well-known/jwks.json。
注意 keys 是一个数组,而且每把公钥都有 kid ——
这正是密钥轮换的关键。
离线验签的完整流程
签发方(持有私钥)
│ 签发令牌,header 里带上 kid
↓
验证方启动
│ ① 拉取一次 JWKS,本地缓存
│ ② 之后每个令牌:
│ 取 header.kid → 在缓存里找到对应公钥
│ 用公钥验签 → 全程不需要联网
↓
验证通过
这样验证方的可用性就和签发方解耦了:
签发方短暂宕机、网络中断,都不影响已经在运行的验证方。
代价是 JWKS 缓存需要设置有效期(比如几分钟到几小时),
并配合 Cache-Control 减少重复拉取。
密钥轮换为什么需要 kid
如果 JWKS 里只有一把公钥,轮换时就会很尴尬: 新令牌用新密钥签,但缓存里还是旧公钥,验证失败; 等缓存刷新,新令牌又能过了,但旧令牌全挂。
用 kid 就平滑得多:
- 先把新公钥加进 JWKS,旧的先别删
- 签发方切换成用新私钥签发,header 里带新
kid - 验证方按
kid查表,新旧令牌都能验过 - 等旧令牌全部过期后,再从 JWKS 里移除旧公钥
几个必须注意的安全点
JWT 的坑大多不在密码学上,而在「该校验的没校验」:
-
算法白名单。
早期库支持
alg: none(无签名),并且会「按令牌说的算法来验证」。 攻击者把 header 改成none、去掉签名,就能伪造任意令牌。 现在成熟库都要求服务端显式指定允许的算法。 - 算法混淆攻击。 如果服务端期望 RS256(非对称),但库允许按 header 切换成 HS256(对称), 攻击者就能拿公开的 RSA 公钥当 HMAC 密钥去签名, 而服务端正好用同一把公钥去验 —— 验过了。 防御方式同样是固定算法族。
-
必须校验
aud。 如果一个系统签发的所有令牌都共用同一把密钥,只是用途不同, 那么 A 用途的令牌就能直接拿去访问 B 用途的接口。 给不同用途的令牌设置不同的aud并强制校验,可以把它隔离开。 -
别忘
exp/iss。 过期时间和签发者校验是基本要求,但确实有实现会漏。
JWT 不适合做什么
最大的局限是无法即时吊销。令牌一旦签发,在 exp 之前一直有效 ——
因为验证方根本不联系签发方,也就无从得知「这个令牌刚刚被作废了」。
常见的应对:
- 有效期设短(分钟到小时级),配合刷新令牌续期
- 刷新令牌做成一次性轮换的,用过即失效,这样被盗用能被发现
- 确实需要即时吊销时,引入黑名单 —— 但这就又变回有状态了,等于放弃 JWT 的主要优势
我的体会是:JWT 不是「更好的 session」,它是为 验证方分散、且可能离线的场景准备的工具。 如果验证方本来就能随时访问中心存储,传统会话反而更简单也更可控。