JWT令牌解码器
解码JSON Web Token并查看内容。
Decoding only — the signature is not verified. Nothing is sent anywhere.
概述
一个 JWT 是三段用点分隔的 base64url 内容:一个说明它如何被签名的头部、一份由声明构成的载荷,以及覆盖前两段的签名。这里的编码不是加密。任何持有这个令牌的人都能读到里面每一条声明,这一点值得在往载荷里放任何东西之前先记牢。
这个工具解码前两段并把它们格式化,然后单独取出三个时间声明,从 Unix 秒换算成你能直接读懂的时间戳。它不验证签名——下面会说明为什么这是一个正确的选择,而不是一处缺失的功能。
使用方法
- 粘贴令牌,也就是
Bearer之后的全部内容——这个方案名本身并不属于 JWT。 - 读 header,看算法,以及密钥 id(如果存在的话)。
- 读 payload,看里面各条声明。
- 检查时间戳表格里的
iat、nbf、exp以及过期判定。
已注册的声明
| 声明 | 名称 | 说明 |
|---|---|---|
iss | 签发方 | 谁签出了这个令牌。你的服务器应该拿它对一份白名单做校验 |
sub | 主体 | 令牌描述的是谁。通常是一个稳定的用户 id,而不是邮箱 |
aud | 受众 | 令牌是给谁用的。为另一个受众签发的令牌必须被拒绝 |
exp | 过期时间 | Unix 秒。过了这个时刻就拒绝 |
nbf | 生效时间 | Unix 秒。早于这个时刻就拒绝 |
iat | 签发时间 | Unix 秒。适合用来实现「超过 N 就重新认证」 |
jti | JWT ID | 唯一 id,好让一个令牌可以被吊销或者做重放检查 |
这七个在规范里全部是可选的。一个没有 exp 的令牌,自己永远不会过期,看到这种情况时,值得意识到这是一个有意做出的设计决定。
为什么不提供签名验证
验证签名需要密钥:HS256 要共享密钥,RS256 和 ES256 要公钥。在对称的那种情形下,用来验证的密钥同时也是用来签名的密钥——所以一个提供验证功能的页面,等于是在要你粘贴那把能让任何人替你的系统签发令牌的凭据。这件事没有任何一种安全的做法。
解码本身仍然值得做。大多数 JWT 排错其实就是在问「这个令牌到底说了什么」——受众不对、少了某个作用域、exp 是五分钟之前、sub 是个邮箱而你的代码期待的是 UUID。这些都不需要密钥就能看见。
解码无法告诉你的,是这个令牌是不是真的。你收到的 JWT,在你的服务器用一把它信任的密钥核对过签名之前,只是一个声称,而不是一个事实。
alg 字段不是你的验证方该外包出去的决定
头部里的 alg 描述的是发送方用了什么。一个读取 alg 然后据此挑选算法的验证方,是在信任攻击者控制的输入,由此产生两种经典的破法:
alg: none。未加密的 JWT 在规范里是合法的。一个遵从它的验证方,会接受任何带着空签名的载荷。RS256被换成HS256。如果某个库拿公钥当 HMAC 的密钥来用,那么知道你公钥的攻击者——那东西本来就是公开的——就能签出你会接受的令牌。
这两个都是配置问题,不是密码学问题。请在你的验证方里把预期算法钉死,拒绝其他一切,而不是从令牌里读出来。
示例
- 一个你解释不了的 401。 解码之后检查
aud。为某一个 API 签发、却拿去请求另一个 API 的令牌是最常见的原因,而错误信息通常不会这么告诉你。 - 时不时才出现的 401。 拿
exp和iat比一下。很短的生命周期,加上一个会缓存令牌的客户端,天生就会时好时坏。 - 一个本该生效的权限。 在载荷里找
scope或roles。如果这条声明根本不存在,那么问题出在签发方,而不在你的授权代码里。 - 审计自己往令牌里放了什么。 解码一个你自己签发的。里面的任何东西——邮箱地址、内部 id、功能开关——凡是持有该令牌的人都能读到,包括存着它的那个浏览器。
令牌在浏览器里应该放在哪里
每次有人解码了一个令牌、发现它是可读的,都会问到这个。简短的答案是:因为 JWT 是一种 Bearer 凭据,所以任何能读到它的东西都能直接使用它。
localStorage 可以被该源上的任何脚本读取,也就是说一次 XSS 会把令牌一起带走。它同时也永远不会被自动发送,所以你不会通过它被 CSRF——你是拿一类缺陷换了另一类缺陷。
HttpOnly 的 Cookie 完全不能被脚本读取,所以 XSS 偷不走它。它会被自动发送,所以它需要 SameSite=Lax 或 Strict 再加上 Secure,而且任何会改变状态的端点都需要自己的一套 CSRF 防御。
对浏览器应用来说,通常的结论是:用一个只存在于内存里的短生命周期访问令牌,通过一个 HttpOnly、Secure、带 SameSite 的刷新令牌 Cookie 来续期。这样一次 XSS 拿到的是一个只剩几分钟寿命的令牌,而不是一份能用好几周的凭据,而那个长生命周期的机密从 JavaScript 里根本触及不到。
不管你最后选哪一种,真正在起作用的是 exp。一个生命周期一小时、又没有吊销列表的令牌,在被偷走之后仍然完整有效一小时,客户端那边做什么都改变不了这一点。
说明
解码之前会先补上填充,因为 base64url 通常会省略它。从日志里复制出来的令牌有时候会带着填充一起过来,两种形式在这里都能用。
过期判定用的是你本地的时钟,所以它只是图个方便,不是权威结论。如果你需要和某个特定时刻做比较,请把原始的 exp 值交给 Unix 时间 换算。
原则上载荷并不一定是 JSON——规范允许任何内容类型——但现实中每一个 JWT 都携带一个 JSON 对象,而一个无法按 JSON 解析的载荷会以原始文本显示出来,不会被隐藏掉。