理解 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 定义的标准声明有特定的名字(iss、sub、aud、exp、nbf、iat、jti);也允许自定义声明,常用的有 scope、roles、tenant 等。
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 都属于下面四种之一。修法每一次都一样。
-
Token 过期。客户端与服务端对当前时间有分歧,或
exp在过去,因为 token 早于配置的寿命就签发了。解码 token;查exp;和服务端时钟对比。当时钟错了修 NTP;token 过期就刷新。 -
错误的 audience。为服务 A 签的 token 被出示给服务 B。解码 token;查
aud;如果它不包含你的服务标识符就拒绝。生产级 JWT 库通常都有audience参数;确认每一次 verify 调用都传它。 -
算法混淆。攻击者把一个
alg: HS256的 token 提交给期待 RS256 的服务,赌该服务会用本该做 RS256 校验的公钥当作 HMAC 密钥。修法两点:在 verify 步骤固定算法(verify(token, key, algorithms=["RS256"])),并且永远不要把同一把密钥既当 HMAC 密钥又当 RSA 私钥。 -
来自 base64url 归一化的签名不匹配。JWT 签名是对
base64url(header) + "." + base64url(payload)的精确字节序列算的;任何缺失或多余的 padding、把-和_改成+和/、或在签名前归一化 Unicode,都会产生不同的签名。用能处理规范形式的库;如果手动调试,把精确的段字节(不是解码后的 JSON)拷进 verifier。
五分钟调试法
本站的 JWT 解码工具 就是为这种场景造的:粘进去 token,一眼看到算法、过期时间、audience 和所有声明。对于签—验调试,再配上 Base64 工具(对比 header / payload 段的精确字节表示)与 哈希生成器(给定已知 HMAC 密钥时算出签名应该是什么)。
- 把出问题的 token 粘进 JWT 解码工具。记下算法与过期时间。
- 如果是
none,这就是紧急状态:有人签了一个无签名 token,并且有服务接受了它。 - 如果 token 已过期,刷新再试;如果 token 还活着,查 audience 与 issuer。
- 如果 audience 与 issuer 都对但校验仍然失败,用你的密钥在哈希生成器中重算签名并和签名段对比;不一致就说明 base64url 归一化或 padding 漂移。
- 签名对但消费方仍然拒,多半是消费方在检查
nbf、iat或你忘了加的自定义声明。
收尾
JWT 理论简单,实战凶险。我们部署时永远配的两道安全网:在 verifier 固定算法,以及用一个小的时钟偏移窗口检查 exp。其他一切——密钥轮换、audience 隔离、重放保护——都从这两条流出。参考资料:JWT 解码工具 用于检验、JSON 指南 用于底层序列化规则、RFC 7519 用于真正的规范。