使用方法
- 把令牌整段粘进上面的输入框,长度上限 20000 字符。示例令牌已经预置在框里,打开页面就能看到解码结果,不需要先点按钮。
- 点「解码令牌」,或者在输入框里按 Ctrl + Enter(macOS 为 Cmd + Enter)立即重算。这里刻意不用裸 Enter 触发,因为令牌经常是从日志里复制出来、需要手动删掉换行和空格,Enter 必须留给编辑。
- 依次看三块内容:头部给出 alg 与 typ,载荷给出业务声明,签名段只原样显示——它是一段二进制摘要,不是 JSON,解成文本只会得到乱码。
- 在「载荷声明明细」里核对时间。exp、nbf、iat 都是 RFC 7519 定义的 NumericDate,单位是秒;本页同时给出 UTC 与北京时间两种读法,避免按本机时区误判「是不是已经过期」。
- 可疑令牌再往下看「令牌字符逐位索引」:它把每个字符的位置、所属段和判定列出来,默认显示前 12 个,点「加载更多」每次追加 24 个。想离线分析就点「导出字符索引 CSV」,或点「复制对照表」把当前结果以制表符分隔粘进表格。
这个 JWT解码工具能解决什么问题
一是核对登录态到底带了什么。前端拿到 401,第一反应常常是「令牌是不是没带上权限」。把令牌粘进来,payload 里的 role、scope、tenant 一类的自定义声明会直接显示出来,不用再翻后端的签发代码。
二是排查时间类的鉴权失败。exp 过期、nbf 还没到、iat 在服务器时钟之后,这三类问题都会表现为「令牌看起来没问题但就是被拒」。本页把三个时间字段都换算成可读时间,并额外算出 exp − iat 的有效期长度,一秒就能看出签发方给的是一小时还是五分钟。
三是审 alg 有没有被改过。alg 决定签名怎么算,权威做法是服务端固定允许的算法、忽略令牌里写的 alg。用这个 JWT解码工具先看一眼 alg 是 HS256 还是 none,可以在排查历史漏洞(alg=none 绕过)时快速确认令牌本身长什么样。
四是确认段数结构。JWS 紧凑序列化是 3 段,JWE 是 5 段。收到一个 5 段的串却当成 JWT 去解,只有先看清段数才能少走弯路。
本页不做这些事:不校验签名、不解密 JWE、不判断令牌是否被吊销、不联网查询签发方公钥、不生成令牌。要自己拼一个令牌请用「JWT编码」;只想处理其中一段的 Base64URL 文本请用「Base64解码」。
解码原理:Base64URL 还原、UTF-8 严格解码与 NumericDate
第一步是分段。按 RFC 7519 §3 的紧凑序列化,一个令牌由三段 Base64URL 文本用英文句点连接而成:头部、载荷、签名。工具用 token.split('.') 切分,段数不等于 3 就直接报错,不会去猜你贴的是哪种结构。这一步同时决定了后面每一步的输入长度。
第二步是 Base64URL 归一化。Base64URL 是 RFC 4648 §5 定义的字母表变体,和 §4 的标准 Base64 相比只有两个字符不同:下标 62 位把 + 换成 -,下标 63 位把 / 换成 _;为了让结果能安全地放进 URL 和文件名,末尾的填充符 = 通常被去掉。所以解码前必须先反向替换回来,再按 4 的倍数补齐。补位规则只有四种情况:长度除以 4 余 0 不补、余 2 补 2 个、余 3 补 1 个、余 1 属于非法输入(合法 Base64 不可能出现余 1)。示例令牌的三段长度分别是 36、180、43,其中 43 % 4 = 3,所以签名段要补 1 个 =,另两段不补。
第三步是字节到文本。atob() 只把 Base64 解成「每个字符的低 8 位就是一个字节」的二进制字符串,如果直接拿它当文本用,浏览器会按 Latin-1 解释,中文立刻变成乱码。所以工具先把每个字符码位收进 Uint8Array,再交给 new TextDecoder('utf-8', { fatal: true }) 解码。fatal: true 是这里最关键的一个取舍:默认值是 false,遇到非法字节序列会悄悄替换成 U+FFFD(也就是 �),让人误以为解码成功;打开 fatal 之后非法序列直接抛错,页面就能明确告诉你「这一段不是合法的 UTF-8 文本」,而不是给你一屏乱码。
第四步才是 JSON.parse。RFC 7519 §3 规定头部与载荷都是 JSON 对象,所以解析失败基本只有三种原因:令牌被截断、粘贴时混进了空格或换行、或者那一段本来就不是 JSON(例如把 JWE 的密文段贴了进来)。本页只接受 JSON 对象,顶层是数组或字符串都会报错,因为那不符合 JWT 的结构定义。
时间字段的单位是最大的坑。RFC 7519 §2 把 NumericDate 定义为「自 1970-01-01T00:00:00Z 起的秒数」,不是毫秒。JavaScript 的 new Date() 接收的是毫秒,因此换算必须乘 1000。示例令牌的 exp 是 1790816400,乘 1000 后是 2026-10-01 09:00:00(北京);如果直接把 1790816400 丢给 new Date(),会显示成 1970 年前后,差了三个数量级。这一条也是很多「令牌没过期却被判过期」的现场根因。
最后是为什么不做验签。签名段的计算必然需要密钥:HS256 用双方共享的密钥,RS256、ES256 用签发方的私钥配公钥。浏览器里既没有你的密钥,也没有你信任的公钥列表,所以这个 JWT解码工具只能回答「令牌里写了什么」,不能回答「令牌是不是真的」。要验签请在服务端做,顺序是:先固定允许的算法(不要读令牌里的 alg 字段),再用公钥或密钥验签,最后才校验 exp、nbf、iss、aud。顺序颠倒会直接掉进 alg=none 与算法混淆两类历史漏洞。
完整示例(数值仅作演示)
示例令牌就写在页面输入框里,三段长度分别是 36、180、43 个字符,连同一个句点合计 261 个字符。用上面四步走一遍,得到的中间量如下:
- 头部段解码后是 27 字节的 JSON:
{"alg":"HS256","typ":"JWT"}; - 载荷段解码后是 135 字节的 JSON,含 8 个声明;
- 签名段解码后是 32 字节,正好是 HMAC-SHA256 的摘要长度,它不是 UTF-8 文本;
- 整串只有 1 个字符
-出现在签名段,没有任何_——这就是 Base64URL 替换后的样子,换成标准 Base64 时它会变回+; - iat 与 nbf 都是 1790812800,exp 是 1790816400,两者相差 3600 秒,即有效期 60 分钟。
把时间字段按「秒 × 1000 交给 Date」换算后:iat/nbf 是 2026-10-01 00:00:00Z(北京 08:00:00),exp 是 2026-10-01 01:00:00Z(北京 09:00:00)。页面顶部四张统计卡显示的「有效期 3600 秒」和「签名段字节数 32」就是这么来的。
下面是这套示例的逐字节账目,加起来可以核对总数:
| 段 | Base64URL 字符数 | 补 = 个数 | 解码后字节数 | 内容类型 |
|---|---|---|---|---|
| 头部 header | 36 | 0 | 27 | JSON 对象 |
| 载荷 payload | 180 | 0 | 135 | JSON 对象 |
| 签名 signature | 43 | 1 | 32 | 二进制摘要 |
| 合计 | 259 + 2 个句点 | 1 | 194 | 整串 261 字符 |
输入范围与对照表
第一张表是 RFC 7519 §4.1 注册的 7 个声明。它们语义固定,第三方库也按这套语义读取;除此之外的键都属于「未注册声明」,含义由签发方自己约定,页面只能原样展示、不能替你解释。
| 声明 | 全称 | 类型 | 本页怎么处理 |
|---|---|---|---|
| iss | Issuer | StringOrURI | 原样展示 |
| sub | Subject | StringOrURI | 原样展示 |
| aud | Audience | StringOrURI 或数组 | 数组会被 JSON 格式化后完整展示 |
| exp | Expiration Time | NumericDate(秒) | 换算成 UTC 与北京时间 |
| nbf | Not Before | NumericDate(秒) | 换算成 UTC 与北京时间 |
| iat | Issued At | NumericDate(秒) | 换算成 UTC 与北京时间 |
| jti | JWT ID | String | 原样展示,用于防重放时才有意义 |
第二张表是两类字母表的差异。整段令牌里出现 - 或 _ 不是加密、也不是压缩,只是换了一张字母表:
| 下标 | 标准 Base64(RFC 4648 §4) | Base64URL(RFC 4648 §5) | 说明 |
|---|---|---|---|
| 0–61 | A–Z a–z 0–9 | 完全相同 | 62 个字符一字不差 |
| 62 | + | - | 本页解码前替换回 + |
| 63 | / | _ | 本页解码前替换回 / |
| 填充 | = | 通常省略 | 按长度 % 4 补回 |
第三张表是补位规则,也是判断「这段到底能不能解」的唯一依据。一个 Base64 字符携带 6 位信息,每 3 字节被编成 4 个字符,所以合法长度一定是 4 的倍数:
| 长度 % 4 | 补 = 个数 | 是否合法 | 举例 |
|---|---|---|---|
| 0 | 0 | 合法 | 示例载荷段 180 个字符 |
| 2 | 2 | 合法 | 补成 4 的倍数再解 |
| 3 | 1 | 合法 | 示例签名段 43 个字符 |
| 1 | 3 | 非法 | 直接报错,不再尝试 |
结果边界、时间换算与舍入说明
- 输入上限 20000 字符。超过之后输入框不再接收新内容,已经在里面的内容不会被截断。一个头部加载荷正常的业务令牌通常在 200 到 2000 字符之间,20000 足够放下绝大多数场景。
- 段数必须是 3。2 段、4 段、5 段都会报错,并会提示你 5 段通常意味着这是 JWE 而不是 JWS。工具不会自作主张地丢掉多余段落。
- 载荷段必须是 UTF-8 文本。RFC 7519 规定头部与载荷是 JSON,JSON 在传输中按 UTF-8 编码。如果某一段的原始字节不是合法 UTF-8(例如把签名段或 JWE 密文段贴了进来),页面会明确报「不是合法 UTF-8」而不是显示乱码。
- 顶层必须是 JSON 对象。解析出来是数组、数字或字符串都会报错,因为 JWT 声明集(Claims Set)的定义就是 JSON 对象。
- 时间字段不做任何舍入。NumericDate 本身是整数秒,工具只做
× 1000的毫秒换算与格式化,不四舍五入、不补齐到分钟。如果某个实现写入了小数秒(例如 1790816400.5),小数部分会被保留并在展示时被截掉,判断是否过期仍以原始值为准。 - 时间只算「显示」,不算「判定」。工具标出 exp 的时刻,但不会告诉你令牌现在是否失效——是否过期取决于你所在机器与签发方的时钟差异。RFC 7519 §4.1.4 明确的是「当前时间大于或等于 exp 即视为过期」,实践中通常再留 60 秒到 300 秒的时钟容差,具体数值 RFC 没有规定。
- 秒与毫秒差 1000 倍。把秒当成毫秒喂给 Date,会得到 1970 年前后的时间;把毫秒当成秒,会得到几万年后的时间。本页按 RFC「秒」处理,并在卡片上标出「秒」这个单位。
- 超过 8.64×1015 毫秒的数值无法表示。这是 JavaScript Date 的可表示上限(约公元 275760 年);再大的 NumericDate 页面会显示「超出可表示范围」而不是给出一个错误日期。
- 重复键按「后者胜」处理。JSON.parse 遇到同名键会保留最后一个,这与 RFC 8259 给出的实现建议一致。如果令牌里出现两次 exp,页面展示的是第二个值,不会提示你存在冲突。
- 签名段不做解码展示。它是二进制摘要,按 UTF-8 解成文本一定是乱码,所以页面只给出原样字符与字节数。想核对签名请用「证书解码」或服务端的验签接口。
常见问题
解出来的内容能证明令牌是真的吗?
不能。头部与载荷只是 Base64URL 编码,任何拿到令牌的人都能把载荷改掉再重新编码。能不能证明真实性,取决于签名段能否用签发方的密钥验证通过,而验证必须在服务端做。本页的解码结果只用于查看内容,不能作为鉴权依据。
为什么不需要输入密钥或公钥?
因为不需要密钥就能读的部分只有头部与载荷——它们没有加密,只是换了一副字符表。签名段本身也不需要密钥就能拿出来看,只是要算出并验证它才需要密钥。页面刻意只做前三步,不做第四步验签,避免给出「验签通过」这种它没有能力给出的结论。
为什么提示「不是合法 UTF-8」?
说明该段的原始字节不可能被解释成 UTF-8 文本。常见原因有三个:贴错段(把签名段或 JWE 密文段贴了进来)、令牌被截断、粘贴时混入了全角空格或中文标点导致 Base64URL 字符校验失败。检查方法是看段长度:合法的 Base64URL 段长度 % 4 不可能是 1。
为什么有的令牌是 5 段?
那是 JWE(RFC 7516)的紧凑序列化:5 段分别是受保护头、加密密钥、初始化向量、密文、认证标签,载荷是加密的密文而不是 JSON。JWE 的正确做法是先解密再读内容,把它的密文段塞进 JWT解码工具一定失败,页面会在段数检查这一步就拦下来并提示你段数不对。
exp 显示的时间和我本地看到的不一样?
先确认单位:NumericDate 是秒,不是毫秒,本页已经乘过 1000。再确认时区:页面固定给出 UTC 与北京时间两种读法,如果你的机器在别的时区,本地时间会与页面显示的北京时间不同,这是正常的。真正影响鉴权的是服务器与签发方之间的时钟差,不是你的浏览器时区。
alg 写着 none 意味着什么?
表示这份令牌声明自己「没有签名」。历史上不少实现会直接读取令牌里的 alg 并据此决定要不要验签,于是把 alg 改成 none、再删掉签名段就能绕过校验,这类问题在 RFC 7515 §10.1 与 CVE-2015-9235 里有记录。正确做法是服务端固定允许的算法列表,不采信令牌自报的 alg;本页只把这个值展示出来,不做任何判断。
令牌里的 - 和 _ 是加密产生的吗?
不是。它们只是 Base64URL 换掉的两个字母表字符:下标 62 的 + 变成 -、下标 63 的 / 变成 _。页面下方的字符逐位索引表会指出每个字符的位置和所属段,方便你对照。看到 - 就以为数据被混淆,是排查时很常见的误判。
为什么签名段解出来是一串乱码?
因为它本来就不是文本,而是一段二进制摘要:HS256 是 32 字节、HS384 是 48 字节、HS512 是 64 字节、RS256 与 ES256 的长度由曲线决定。二进制字节里出现任意 0 到 255 的值,按 UTF-8 解释必然大量非法,所以页面只显示它的原样字符与字节数,不把它当文本解码。
相关工具
- JWT编码:反方向使用,把头部与载荷 JSON 拼成三段式令牌,适合本地造一条测试数据。
- Base64解码:单独处理一段 Base64 文本,不涉及令牌结构,遇到 - 和 _ 时需要先自己替换。
- Base64编码:把任意文本编成 Base64,用来核对某一段解码前的原文。
- Unicode转义反转义工具:载荷里出现 \u4e2d 这类转义时,用它还原成可读字符。
- 证书解码:查看 PEM 证书里的公钥与有效期,排查签名用哪把密钥时配合使用。
- URL解码:令牌常出现在 URL 参数里,取出来之后先做一次百分号解码再看内容。
- 密码力量检测:HS256 的密钥强度直接决定令牌可伪造性,可以用它估一下密钥的字符池与信息量。
- 编码解码:查看全部编码解码工具。