快思学习空间

首页/ 学习笔记/ JWT 与 JWKS

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 就平滑得多:

  1. 先把新公钥加进 JWKS,旧的先别删
  2. 签发方切换成用新私钥签发,header 里带新 kid
  3. 验证方按 kid 查表,新旧令牌都能验过
  4. 等旧令牌全部过期后,再从 JWKS 里移除旧公钥

几个必须注意的安全点

JWT 的坑大多不在密码学上,而在「该校验的没校验」:

  • 算法白名单。 早期库支持 alg: none(无签名),并且会「按令牌说的算法来验证」。 攻击者把 header 改成 none、去掉签名,就能伪造任意令牌。 现在成熟库都要求服务端显式指定允许的算法。
  • 算法混淆攻击。 如果服务端期望 RS256(非对称),但库允许按 header 切换成 HS256(对称), 攻击者就能拿公开的 RSA 公钥当 HMAC 密钥去签名, 而服务端正好用同一把公钥去验 —— 验过了。 防御方式同样是固定算法族。
  • 必须校验 aud。 如果一个系统签发的所有令牌都共用同一把密钥,只是用途不同, 那么 A 用途的令牌就能直接拿去访问 B 用途的接口。 给不同用途的令牌设置不同的 aud 并强制校验,可以把它隔离开。
  • 别忘 exp / iss。 过期时间和签发者校验是基本要求,但确实有实现会漏。

JWT 不适合做什么

最大的局限是无法即时吊销。令牌一旦签发,在 exp 之前一直有效 —— 因为验证方根本不联系签发方,也就无从得知「这个令牌刚刚被作废了」。

常见的应对:

  • 有效期设短(分钟到小时级),配合刷新令牌续期
  • 刷新令牌做成一次性轮换的,用过即失效,这样被盗用能被发现
  • 确实需要即时吊销时,引入黑名单 —— 但这就又变回有状态了,等于放弃 JWT 的主要优势

我的体会是:JWT 不是「更好的 session」,它是为 验证方分散、且可能离线的场景准备的工具。 如果验证方本来就能随时访问中心存储,传统会话反而更简单也更可控。

← 上一篇:mDNS 服务发现 下一篇:文件秒传背后的判重策略 →