DevFormatLab
← 返回博客列表

理解 JWT —— 一份实战指南

作者 DevFormatLab Editorial·8 分钟阅读
JWT认证安全教程

JSON Web Token(JWT)已经成为 Web API、OAuth 2.0 流程和微服务无状态认证的事实标准。它看起来是三段用点号连接的 base64url 串,但每一段在信任模型里都对应着明确的角色。生产里调试过上千次 JWT 相关问题之后,我们整理出了几乎所有失败都遵循的模式,以及对应的修法。

JWT 的三段

JWT 是用点号分隔的三段 base64url 字符串:header.payload.signature。每段都有明确的职责。

Header声明算法与 token 类型。最简有用的 header 像这样:

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

Payload承载声明(claims)——关于持有人的断言。RFC 7519 定义的标准声明有特定的名字(isssubaudexpnbfiatjti);也允许自定义声明,常用的有 scoperolestenant 等。

Signature是密码学证明。它基于 base64url(header) + "." + base64url(payload),按 header.alg 声明的算法,用一个密钥(HMAC 系列)或私钥(RSA / ECDSA)算出。验证方用同样的输入加上自己的密钥或公钥重算签名,匹配上即认为 token 可信。

逐一解释每个声明

RFC 7519 的七个标准声明,外加几个常见的习惯用法扩展。本站的 JWT 解码工具 把它们都解析出来并在一个易读的侧栏里展示。

  • iss(issuer):谁签发的 token。"https://auth.example.com" 是典型值。针对白名单校验;不要相信任何 iss 不在预期里的 token。
  • sub(subject):token 是关于谁的——通常是用户 ID、邮箱或一个不透明标识符。需要校验。
  • aud(audience):token 是给谁的。可以是字符串或数组。针对你的服务标识符校验;如果 aud 里没有你就拒绝。
  • exp(expiration):一个 epoch 秒,到此之后 token 不应再被信任。拒绝 exp 已过去的 token,留少量时钟偏移(典型 60s)。
  • nbf(not before):一个 epoch 秒,在此之前 token 不应被信任。适合延迟激活的 token。
  • iat(issued at):token 是何时签发的。适合缓存失效与最大 token 生存期策略。
  • jti(JWT ID):本 token 的唯一标识,用于重放检测。如果你的服务实现一次性 token 策略,必填。

HS256 vs RS256 vs ES256 —— 选清楚

算法的选择是一个项目开始时只做一次的安全决策,所以值得一次做对。

HS256 / HS384 / HS512(HMAC)
同一个对称密钥既用来签也用来验。快、简单、无需密钥基础设施。代价:任何能验 token 的人也能签 token。当签发方和验证方是同一服务,或共享受信密钥存储时,HS256 是合适的。当多个服务互相校验 token 时,它几乎从来不是正确的选择——因为现在每个验证方都有能力给所有其他验证方伪造 token。
RS256 / RS384 / RS512(RSA)
签发方持私钥;验证方持公钥。验证方可验不可签。这是联邦身份、单点登录、OAuth 2.0 服务、以及任何消费者不该拥有签发能力的场景的正确选择。代价是非对称密钥基础设施:密钥轮换、JWKS endpoint,以及不能让私钥泄露的运维负担。
ES256 / ES384 / ES512(ECDSA)
RSA 签名的椭圆曲线版本。更短的签名、等价的强度、更快的验证、更慢的签发。大多数现代 JWT 部署都在向 ES256 迁移,目的就是体积与性能。
none
token 没有签名。任何接受 alg: none 的消费者都会接受含任意声明的伪造 token。这个选项只为调试存在,每个生产 JWT 库都应该通过配置把它移除。本站的 JWT 解码工具 把任何 alg: none 的 token 标红,你不可能错过这个警告。

四种常见失败模式

我们见过的几乎每一个生产 JWT bug 都属于下面四种之一。修法每一次都一样。

  1. Token 过期。客户端与服务端对当前时间有分歧,或 exp 在过去,因为 token 早于配置的寿命就签发了。解码 token;查 exp;和服务端时钟对比。当时钟错了修 NTP;token 过期就刷新。

  2. 错误的 audience。为服务 A 签的 token 被出示给服务 B。解码 token;查 aud;如果它不包含你的服务标识符就拒绝。生产级 JWT 库通常都有 audience 参数;确认每一次 verify 调用都传它。

  3. 算法混淆。攻击者把一个 alg: HS256 的 token 提交给期待 RS256 的服务,赌该服务会用本该做 RS256 校验的公钥当作 HMAC 密钥。修法两点:在 verify 步骤固定算法(verify(token, key, algorithms=["RS256"])),并且永远不要把同一把密钥既当 HMAC 密钥又当 RSA 私钥。

  4. 来自 base64url 归一化的签名不匹配。JWT 签名是对 base64url(header) + "." + base64url(payload) 的精确字节序列算的;任何缺失或多余的 padding、把 -_ 改成 +/、或在签名前归一化 Unicode,都会产生不同的签名。用能处理规范形式的库;如果手动调试,把精确的段字节(不是解码后的 JSON)拷进 verifier。

五分钟调试法

本站的 JWT 解码工具 就是为这种场景造的:粘进去 token,一眼看到算法、过期时间、audience 和所有声明。对于签—验调试,再配上 Base64 工具(对比 header / payload 段的精确字节表示)与 哈希生成器(给定已知 HMAC 密钥时算出签名应该是什么)。

  1. 把出问题的 token 粘进 JWT 解码工具。记下算法与过期时间。
  2. 如果是 none,这就是紧急状态:有人签了一个无签名 token,并且有服务接受了它。
  3. 如果 token 已过期,刷新再试;如果 token 还活着,查 audience 与 issuer。
  4. 如果 audience 与 issuer 都对但校验仍然失败,用你的密钥在哈希生成器中重算签名并和签名段对比;不一致就说明 base64url 归一化或 padding 漂移。
  5. 签名对但消费方仍然拒,多半是消费方在检查 nbfiat 或你忘了加的自定义声明。

收尾

JWT 理论简单,实战凶险。我们部署时永远配的两道安全网:在 verifier 固定算法,以及用一个小的时钟偏移窗口检查 exp。其他一切——密钥轮换、audience 隔离、重放保护——都从这两条流出。参考资料:JWT 解码工具 用于检验、JSON 指南 用于底层序列化规则、RFC 7519 用于真正的规范。

相关工具