如何解码并检查 JWT 令牌
JSON Web Tokens (JWT) 是现代 Web 应用中处理身份验证最常见的方式。当 auth 出问题时,用户被意外登出、权限不对、API 返回 401,解码 JWT 通常是第一步排查。理解 JWT 的三部分、标准声明、可用于签名的算法以及常见陷阱,会把 auth 调试从巫术变成例行检查。
JWT 简史
JWT 在 2015 年 5 月的 RFC 7519 中被标准化,此前在 IETF 经过多年草案迭代。该格式借鉴了早期紧凑令牌设计(SAML 断言、简单的不透明 cookie),但加入了它们所缺乏的两项:在任何语言中可读的严格 JSON 形态,以及在 URL 参数、HTTP 头和表单字段中不需再次转义就能存活的 base64url-safe 编码。配套规范,JWS(RFC 7515)用于签名、JWE(RFC 7516)用于加密、JWA(RFC 7518)用于算法名称,共同构成 JOSE(JavaScript Object Signing and Encryption)家族。
不久之后,OAuth 2.0 和 OpenID Connect 把 JWT 作为默认令牌格式,这就是为什么几乎所有现代 auth 提供商(Auth0、Okta、Cognito、Keycloak、Firebase、Supabase、Clerk)今天都签发 JWT。自包含令牌与无状态后端的组合非常自然地适配微服务和 API 网关。缺点是 JWT 出了名的容易被误用,过去十年里在没有仔细验证算法的库中出现了源源不断的 CVE。
JWT 内部是什么
JWT 有三部分,以点分隔:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U
头部:包含算法(HS256、RS256 等)和令牌类型。
{"alg": "HS256", "typ": "JWT"}
负载:包含关于用户和令牌的声明(数据断言)。
{"sub": "1234567890", "name": "Alice", "exp": 1700000000}
签名,一段密码学哈希,用于验证令牌未被篡改。没有签名密钥你无法读取它。
每一段都以 base64url 编码,意味着它使用 - 与 _ 替代 + 与 /,并省略尾部的 = 填充。Base64url 不是加密;把中间这段粘进任何解码器都会暴露负载。这是设计使然:中间段被设计成可在沿途的服务读取,签名才是证明真实性的部分。
常见 JWT 声明
标准声明在 IANA 注册并在 RFC 7519 中定义。大多数是可选的,但下面这些几乎总会出现。
| 声明 | 全名 | 内容 |
|---|---|---|
sub | Subject(主体) | 用户 ID 或标识符 |
exp | Expiration(过期) | 令牌过期时的 Unix 时间戳 |
iat | Issued At(签发于) | 令牌创建时的 Unix 时间戳 |
iss | Issuer(签发者) | 谁创建了令牌(你的 auth 服务器) |
aud | Audience(受众) | 令牌发给谁 |
nbf | Not Before(不早于) | 令牌在此时间前不生效 |
jti | JWT ID | 令牌的唯一标识符 |
azp | Authorized Party | 令牌签发给的那一方(OIDC) |
scope / scp | OAuth 范围 | 授予的权限,通常以空格分隔 |
email | 邮箱 | 标准 OIDC 用户标识 |
name | 名字 | 显示名(OIDC) |
nonce | Nonce | OIDC 防重放值 |
kid(头部) | Key ID | 使用了哪把签名密钥(用于 JWKS 查找) |
除标准集合外,应用会加入自己的自定义声明(roles、tenant_id、feature_flags、permissions)。自定义声明名默认不带命名空间,因此两个不同服务可能用同一名字表达不同含义;OIDC 约定以 URI 前缀(https://myapp.com/roles)来避免碰撞。
如何解码 JWT
- 粘贴你的令牌:把完整 JWT(header.payload.signature 格式)输入解码器。基于浏览器的解码器在本地处理,令牌从不离开页面。
- 查看解码后的部分:工具以格式化的 JSON 显示头部(算法)、负载(声明)与签名,时间戳同时以 Unix 整数和可读日期展示。
- 检查声明:检查过期时间、签发者、主体、受众,以及驱动授权逻辑的任何自定义声明。
- 对照预期:将签发者与你配置的 auth 提供商相互对照,将受众与令牌要发送到的 API 对照,把任何角色/范围声明与用户应有权限对照。
- 时光旅行测试:把鼠标悬停在
iat、nbf与exp上,看令牌当前是否有效、即将过期,还是发出已久,你的时钟漂移容差已无法覆盖。
签名算法
并非所有 JWT 都使用同一种加密。alg 头告诉你签名属于哪个家族,而每个家族具有非常不同的安全特性。
| 算法 | 家族 | 密钥类型 | 何时选择 |
|---|---|---|---|
HS256 | HMAC | 共享密钥 | 单服务应用;切勿跨团队共享密钥 |
HS384 / HS512 | HMAC | 共享密钥 | 与 HS256 相同,摘要更长 |
RS256 | RSA | 公/私钥对 | OIDC 最常用;验证方只需要公钥 |
RS384 / RS512 | RSA | 密钥对 | 与 RS256 相同,密钥更大 |
PS256 / PS384 / PS512 | RSA-PSS | 密钥对 | 现代 RSA,新部署推荐优先于 RS |
ES256 / ES384 / ES512 | ECDSA | 椭圆曲线密钥对 | 密钥比 RSA 小,验证更快 |
EdDSA | Ed25519 | Edwards 曲线密钥对 | 最新、最小、最快;尚未普及 |
none | 无 | 无 | 生产中禁用;一些旧库仍接受 |
非对称算法(RS*、PS*、ES*、EdDSA)允许任何服务仅凭公钥就验证令牌,这就是它们主导 OIDC 的原因。对称(HS*)在单一应用内可以,但要在多个消费者之间轮换或分发会变成噩梦。
用 JWT 调试
令牌过期?检查 exp 声明。把 Unix 时间戳转成可读日期。如果在过去,令牌就过期了,需要刷新。大多数 JWT 库默认拒绝过期令牌;如果你的应用接受它们,那就是一个安全 bug。
权限不对?在负载中查找角色或范围声明。实现因人而异,但常见形如 "role": "admin" 或 "scope": "read write profile"。
用户身份问题?sub 声明标识用户。核对它是否与期望的用户 ID 相符。注意:一些提供商使用不透明 GUID,另一些使用电子邮件;解码器准确显示其中是什么。
令牌不被接受?检查 aud(受众)声明。如果 API 期望特定的受众值而令牌的不同,就会被拒绝。受众不匹配通常是把令牌发到了错的服务的征兆。
部署后出现 401?检查 iss(签发者)声明。新的 auth 提供商租户或被替换的签名密钥都会更改签发者 URL;如果你的验证方仍信任旧的,所有令牌看起来都无效。
时钟漂移问题?如果 iat 略微在未来或 exp 略微在过去,你服务器的时钟可能在漂移。大多数 JWT 库允许几秒余地;若不行,NTP 同步的时钟就能解决。
常见陷阱
- 在没有允许列表的情况下信任
alg头,经典的 JWT 漏洞是服务器接受令牌自称的任何算法。带alg: none(无签名)或alg: HS256(以你的公钥作为秘钥签名)的令牌可伪造任何负载。把验证方固定到期望的确切算法。 - 把秘密放进负载,负载是 base64url 编码而非加密。任何持有令牌的人都能读取。不要包含密码、API 密钥,或任何你不会放进查询字符串的东西。
- 长生命周期令牌没有撤销,30 天的 JWT 在没有令牌黑名单或会话存储的情况下无法撤销。让访问令牌保持短(5-60 分钟),长会话用刷新令牌流程。
- 忘记时钟漂移,在不同时区或时钟漂移的服务器会拒绝本应有效的令牌。允许
exp与nbf有 30-60 秒的余地。 - 不验证就信任签发者,
iss声明是负载的一部分,任何人都能写。验证其值匹配已配置的签发者是必需的;把它放入允许列表,可防止攻击者铸造声称来自你提供商的令牌。 - 跨环境复用 HS256 秘钥,dev 与 prod 使用相同秘钥意味着泄露的 dev 令牌在生产生效。按环境使用不同密钥,理想情况从秘钥管理器获取。
- 把令牌存在 localStorage,localStorage 对任意 JavaScript 可读,因此一个 XSS bug 就能外泄每个用户的令牌。带 SameSite=Lax(或 Strict)的 HttpOnly cookie 是更安全的默认。
- 记录完整令牌,捕获完整 JWT 的应用日志会把令牌泄露给任何有日志访问的人。截断至前 10 个字符,或只记录
jti。 - 忽略
kid轮换,当提供商轮换签名密钥时,新令牌带新的kid头。永久缓存 JWKS 的验证方会开始拒绝有效令牌。在 key-id miss 时重新拉取 JWKS。 - 把 JWT 与会话混用,有的端点用 JWT,有的用 cookie 会话,会导致已登录用户在某条路由上显示未认证的 bug。每个服务挑一种模型。
JWT 的替代方案
JWT 占主导但不是唯一选择。每种替代方案在不同特性上做取舍。
| 机制 | 优势 | 劣势 |
|---|---|---|
| JWT (JWS) | 自包含,跨服务方便 | 无额外状态就无法撤销 |
| 不透明令牌 + 内省 | 容易撤销,隐藏声明 | 每次请求都打到 auth 服务器 |
| 服务端会话 | 模型最简单,即时撤销 | 跨服务难扩展 |
| PASETO | 更安全的 JWT 替代(无 alg 混淆) | 生态更小 |
| Macaroons | 内置衰减(可委托权利) | 库支持有限 |
| OAuth 2.0 + JWT 访问令牌 | API 行业标准 | 规范庞大,易实现错误 |
| OIDC ID 令牌 | 标准用户身份 + JWT | 常与访问令牌混淆 |
| mTLS 客户端证书 | 传输层上最强的认证 | 证书管理开销 |
对大多数团队,选择在 JWT 与不透明令牌之间。当验证需要便宜且离线时,JWT 胜出;当撤销必须即时时,不透明令牌胜出。
隐私与解码器
JWT 解码器完全在你的浏览器中运行。你粘贴的令牌会被分割、base64url 解码,JSON 在没有任何网络请求的情况下被解析并美化。不存在已解码令牌的日志,不存在关于其中声明的分析,也没人能重建你在为谁调试。JWT 常常包含用户标识、电子邮件地址、内部角色名以及租户 ID,这些正是你不愿意发送到陌生人服务器上的元数据。在客户端解码会让这些信息留在你的机器上,这是任何涉及身份验证的调试任务的合理默认。
常见问题
可以用解码器验证 JWT 签名吗?
不能。签名验证需要签名密钥或公钥,它们存在您的服务器上。解码器显示令牌中的内容,但加密验证必须在您的后端进行。生产环境中绝不要信任未经验证的 JWT。
将 JWT 粘贴到在线工具安全吗?
当工具在浏览器中运行时是安全的。浏览器内的解码器本地处理令牌 · 不会向服务器发送任何内容。避免使用会用您的令牌发出网络请求的工具。
exp claim 是什么?
exp(expiration)claim 是一个 Unix 时间戳,指示令牌何时过期。超过此时间后,令牌应被拒绝。始终检查此 claim 来调试认证问题。
JWT 可以加密吗?
标准 JWT(JWS)是签名而非加密的 · 任何人都能解码 payload。JWE(JSON Web Encryption)令牌是加密的,但不太常见。绝不要将敏感数据(密码、机密)放入标准 JWT payload 中。
What is the alg none vulnerability?
Early JWT libraries accepted tokens with an alg header set to "none", meaning the signature could be omitted entirely. An attacker who set this header could forge any payload. Modern libraries reject "none" by default, but legacy systems may still be exposed; always allow-list the expected algorithm rather than trusting the header.
How should I store a JWT on the client?
HttpOnly secure cookies with SameSite=Lax (or Strict) are the safest default; they cannot be read by JavaScript, which mitigates XSS token theft. localStorage is convenient but vulnerable to any XSS bug. Never store long-lived JWTs alongside untrusted scripts.