如何解码并检查 JWT 令牌

· 9 分钟阅读

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 中定义。大多数是可选的,但下面这些几乎总会出现。

声明全名内容
subSubject(主体)用户 ID 或标识符
expExpiration(过期)令牌过期时的 Unix 时间戳
iatIssued At(签发于)令牌创建时的 Unix 时间戳
issIssuer(签发者)谁创建了令牌(你的 auth 服务器)
audAudience(受众)令牌发给谁
nbfNot Before(不早于)令牌在此时间前不生效
jtiJWT ID令牌的唯一标识符
azpAuthorized Party令牌签发给的那一方(OIDC)
scope / scpOAuth 范围授予的权限,通常以空格分隔
email邮箱标准 OIDC 用户标识
name名字显示名(OIDC)
nonceNonceOIDC 防重放值
kid(头部)Key ID使用了哪把签名密钥(用于 JWKS 查找)

除标准集合外,应用会加入自己的自定义声明(rolestenant_idfeature_flagspermissions)。自定义声明名默认不带命名空间,因此两个不同服务可能用同一名字表达不同含义;OIDC 约定以 URI 前缀(https://myapp.com/roles)来避免碰撞。

如何解码 JWT

  1. 粘贴你的令牌:把完整 JWT(header.payload.signature 格式)输入解码器。基于浏览器的解码器在本地处理,令牌从不离开页面。
  2. 查看解码后的部分:工具以格式化的 JSON 显示头部(算法)、负载(声明)与签名,时间戳同时以 Unix 整数和可读日期展示。
  3. 检查声明:检查过期时间、签发者、主体、受众,以及驱动授权逻辑的任何自定义声明。
  4. 对照预期:将签发者与你配置的 auth 提供商相互对照,将受众与令牌要发送到的 API 对照,把任何角色/范围声明与用户应有权限对照。
  5. 时光旅行测试:把鼠标悬停在 iatnbfexp 上,看令牌当前是否有效、即将过期,还是发出已久,你的时钟漂移容差已无法覆盖。

签名算法

并非所有 JWT 都使用同一种加密。alg 头告诉你签名属于哪个家族,而每个家族具有非常不同的安全特性。

算法家族密钥类型何时选择
HS256HMAC共享密钥单服务应用;切勿跨团队共享密钥
HS384 / HS512HMAC共享密钥与 HS256 相同,摘要更长
RS256RSA公/私钥对OIDC 最常用;验证方只需要公钥
RS384 / RS512RSA密钥对与 RS256 相同,密钥更大
PS256 / PS384 / PS512RSA-PSS密钥对现代 RSA,新部署推荐优先于 RS
ES256 / ES384 / ES512ECDSA椭圆曲线密钥对密钥比 RSA 小,验证更快
EdDSAEd25519Edwards 曲线密钥对最新、最小、最快;尚未普及
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 同步的时钟就能解决。

常见陷阱

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.