⌘K 切换工具
SECURITY

JWT令牌解码器

解码JSON Web Token并查看内容。

local
jwt-decoder

Decoding only — the signature is not verified. Nothing is sent anywhere.

§01 关于此工具

概述

一个 JWT 是三段用点分隔的 base64url 内容:一个说明它如何被签名的头部、一份由声明构成的载荷,以及覆盖前两段的签名。这里的编码不是加密。任何持有这个令牌的人都能读到里面每一条声明,这一点值得在往载荷里放任何东西之前先记牢。

这个工具解码前两段并把它们格式化,然后单独取出三个时间声明,从 Unix 秒换算成你能直接读懂的时间戳。它不验证签名——下面会说明为什么这是一个正确的选择,而不是一处缺失的功能。

使用方法

  1. 粘贴令牌,也就是 Bearer 之后的全部内容——这个方案名本身并不属于 JWT。
  2. header,看算法,以及密钥 id(如果存在的话)。
  3. payload,看里面各条声明。
  4. 检查时间戳表格里的 iatnbfexp 以及过期判定。

已注册的声明

声明名称说明
iss签发方谁签出了这个令牌。你的服务器应该拿它对一份白名单做校验
sub主体令牌描述的是谁。通常是一个稳定的用户 id,而不是邮箱
aud受众令牌是给谁用的。为另一个受众签发的令牌必须被拒绝
exp过期时间Unix 秒。过了这个时刻就拒绝
nbf生效时间Unix 秒。早于这个时刻就拒绝
iat签发时间Unix 秒。适合用来实现「超过 N 就重新认证」
jtiJWT ID唯一 id,好让一个令牌可以被吊销或者做重放检查

这七个在规范里全部是可选的。一个没有 exp 的令牌,自己永远不会过期,看到这种情况时,值得意识到这是一个有意做出的设计决定。

为什么不提供签名验证

验证签名需要密钥:HS256 要共享密钥,RS256ES256 要公钥。在对称的那种情形下,用来验证的密钥同时也是用来签名的密钥——所以一个提供验证功能的页面,等于是在要你粘贴那把能让任何人替你的系统签发令牌的凭据。这件事没有任何一种安全的做法。

解码本身仍然值得做。大多数 JWT 排错其实就是在问「这个令牌到底说了什么」——受众不对、少了某个作用域、exp 是五分钟之前、sub 是个邮箱而你的代码期待的是 UUID。这些都不需要密钥就能看见。

解码无法告诉你的,是这个令牌是不是真的。你收到的 JWT,在你的服务器用一把它信任的密钥核对过签名之前,只是一个声称,而不是一个事实。

alg 字段不是你的验证方该外包出去的决定

头部里的 alg 描述的是发送方用了什么。一个读取 alg 然后据此挑选算法的验证方,是在信任攻击者控制的输入,由此产生两种经典的破法:

  • alg: none。未加密的 JWT 在规范里是合法的。一个遵从它的验证方,会接受任何带着空签名的载荷。
  • RS256 被换成 HS256。如果某个库拿公钥当 HMAC 的密钥来用,那么知道你公钥的攻击者——那东西本来就是公开的——就能签出你会接受的令牌。

这两个都是配置问题,不是密码学问题。请在你的验证方里把预期算法钉死,拒绝其他一切,而不是从令牌里读出来。

示例

  • 一个你解释不了的 401。 解码之后检查 aud。为某一个 API 签发、却拿去请求另一个 API 的令牌是最常见的原因,而错误信息通常不会这么告诉你。
  • 时不时才出现的 401。expiat 比一下。很短的生命周期,加上一个会缓存令牌的客户端,天生就会时好时坏。
  • 一个本该生效的权限。 在载荷里找 scoperoles。如果这条声明根本不存在,那么问题出在签发方,而不在你的授权代码里。
  • 审计自己往令牌里放了什么。 解码一个你自己签发的。里面的任何东西——邮箱地址、内部 id、功能开关——凡是持有该令牌的人都能读到,包括存着它的那个浏览器。

令牌在浏览器里应该放在哪里

每次有人解码了一个令牌、发现它是可读的,都会问到这个。简短的答案是:因为 JWT 是一种 Bearer 凭据,所以任何能读到它的东西都能直接使用它。

localStorage 可以被该源上的任何脚本读取,也就是说一次 XSS 会把令牌一起带走。它同时也永远不会被自动发送,所以你不会通过它被 CSRF——你是拿一类缺陷换了另一类缺陷。

HttpOnly 的 Cookie 完全不能被脚本读取,所以 XSS 偷不走它。它被自动发送,所以它需要 SameSite=LaxStrict 再加上 Secure,而且任何会改变状态的端点都需要自己的一套 CSRF 防御。

对浏览器应用来说,通常的结论是:用一个只存在于内存里的短生命周期访问令牌,通过一个 HttpOnlySecure、带 SameSite 的刷新令牌 Cookie 来续期。这样一次 XSS 拿到的是一个只剩几分钟寿命的令牌,而不是一份能用好几周的凭据,而那个长生命周期的机密从 JavaScript 里根本触及不到。

不管你最后选哪一种,真正在起作用的是 exp。一个生命周期一小时、又没有吊销列表的令牌,在被偷走之后仍然完整有效一小时,客户端那边做什么都改变不了这一点。

说明

解码之前会先补上填充,因为 base64url 通常会省略它。从日志里复制出来的令牌有时候会带着填充一起过来,两种形式在这里都能用。

过期判定用的是你本地的时钟,所以它只是图个方便,不是权威结论。如果你需要和某个特定时刻做比较,请把原始的 exp 值交给 Unix 时间 换算。

原则上载荷并不一定是 JSON——规范允许任何内容类型——但现实中每一个 JWT 都携带一个 JSON 对象,而一个无法按 JSON 解析的载荷会以原始文本显示出来,不会被隐藏掉。

FAQ
这个工具会验证签名吗?
不会,而且这是刻意的。验证需要签名密钥,而把签名密钥粘进一个网页,恰恰就是这个工具不该去鼓励的那种错误。解码告诉你的是令牌声称了什么;只有你自己的服务器才能判断这个声称是否为真。
我的令牌会被发送到什么地方吗?
不会。令牌在页面内按点分割,并做 base64url 解码,没有任何网络请求。不过,任何粘贴到别处的令牌都该当作已经作废——躺在剪贴板历史或浏览器会话里的 Bearer 令牌,是一份你已经无法完全掌控的凭据。
为什么它显示 EXPIRED,而我的 API 仍然接受这个令牌?
这个状态是把 exp 和你电脑上的时钟做比较。如果你的时钟不准,或者签发方允许一定的时钟偏差(几分钟是常见做法),两边的判定就会不一致。令牌自己的 exp 值才是权威的那个数字,页面上的标签只是图个方便。
只有两段的令牌能解码吗?
可以。两段既可能是一个合法的未加密 JWT(alg: none),也可能是你粘贴的时候没带上签名。工具需要的是头部和载荷;签名本来就不参与。
载荷看起来是一堆乱码,哪里出了问题?
最常见的情况是这个令牌是被加密而不是被签名的(也就是 JWE,它有五段),或者被什么东西重新编码过——比如从一个会折行的终端里复制,或者从一个把它转义过的 JSON 字符串里复制。JWS 的载荷是纯 base64url,永远能解码成 JSON。