JWT解码

把三段式令牌按 RFC 7519 拆开:头部与载荷用 Base64URL 还原后再按 UTF-8 严格解码成 JSON,并把 exp、iat、nbf 三个秒级时间字段换算成 UTC 与北京时间。本页只解析内容,不做签名验签。

段数3
alg / typHS256 / JWT
载荷声明数8
有效期 exp − iat3600 秒
签名段字节数32

头部 header(Base64URL → UTF-8 → JSON)

{
  "alg": "HS256",
  "typ": "JWT"
}

载荷 payload(Base64URL → UTF-8 → JSON)

{
  "sub": "10086",
  "name": "liuquan",
  "iss": "liuquan.net",
  "aud": "newtools",
  "iat": 1790812800,
  "nbf": 1790812800,
  "exp": 1790816400,
  "role": "admin"
}

签名段(原样展示,它是二进制摘要,不是 JSON,本页不会把它解成文本)

Kn-RDNQ4VeFrAq-dEUDMc44ltloPZNGSN6hLwBnucD0(32 字节)

载荷声明明细

声明值类型说明
sub10086string主体,通常是用户 ID
nameliuquanstring未注册的自定义声明
issliuquan.netstring签发者
audnewtoolsstring接收方
iat1790812800number签发时间:2026-10-01 00:00:00Z(北京 08:00:00)
nbf1790812800number生效时间:2026-10-01 00:00:00Z(北京 08:00:00)
exp1790816400number过期时间:2026-10-01 01:00:00Z(北京 09:00:00)
roleadminstring未注册的自定义声明

令牌字符逐位索引(用来看清 - 和 _ 出现在哪一段)

位置所属段字符判定

令牌输入框里按 Ctrl + Enter(macOS 用 Cmd + Enter)触发解码;Enter 留给换行。示例令牌的签名段是演示用的固定字节,任何真实校验器都会判它无效,请勿用于测试验签逻辑。

使用方法

  1. 把令牌整段粘进上面的输入框,长度上限 20000 字符。示例令牌已经预置在框里,打开页面就能看到解码结果,不需要先点按钮。
  2. 点「解码令牌」,或者在输入框里按 Ctrl + Enter(macOS 为 Cmd + Enter)立即重算。这里刻意不用裸 Enter 触发,因为令牌经常是从日志里复制出来、需要手动删掉换行和空格,Enter 必须留给编辑。
  3. 依次看三块内容:头部给出 alg 与 typ,载荷给出业务声明,签名段只原样显示——它是一段二进制摘要,不是 JSON,解成文本只会得到乱码。
  4. 在「载荷声明明细」里核对时间。exp、nbf、iat 都是 RFC 7519 定义的 NumericDate,单位是秒;本页同时给出 UTC 与北京时间两种读法,避免按本机时区误判「是不是已经过期」。
  5. 可疑令牌再往下看「令牌字符逐位索引」:它把每个字符的位置、所属段和判定列出来,默认显示前 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 个字符。用上面四步走一遍,得到的中间量如下:

把时间字段按「秒 × 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 字符数补 = 个数解码后字节数内容类型
头部 header36027JSON 对象
载荷 payload1800135JSON 对象
签名 signature43132二进制摘要
合计259 + 2 个句点1194整串 261 字符

输入范围与对照表

第一张表是 RFC 7519 §4.1 注册的 7 个声明。它们语义固定,第三方库也按这套语义读取;除此之外的键都属于「未注册声明」,含义由签发方自己约定,页面只能原样展示、不能替你解释。

声明全称类型本页怎么处理
issIssuerStringOrURI原样展示
subSubjectStringOrURI原样展示
audAudienceStringOrURI 或数组数组会被 JSON 格式化后完整展示
expExpiration TimeNumericDate(秒)换算成 UTC 与北京时间
nbfNot BeforeNumericDate(秒)换算成 UTC 与北京时间
iatIssued AtNumericDate(秒)换算成 UTC 与北京时间
jtiJWT IDString原样展示,用于防重放时才有意义

第二张表是两类字母表的差异。整段令牌里出现 - 或 _ 不是加密、也不是压缩,只是换了一张字母表:

下标标准 Base64(RFC 4648 §4)Base64URL(RFC 4648 §5)说明
0–61A–Z a–z 0–9完全相同62 个字符一字不差
62+-本页解码前替换回 +
63/_本页解码前替换回 /
填充=通常省略按长度 % 4 补回

第三张表是补位规则,也是判断「这段到底能不能解」的唯一依据。一个 Base64 字符携带 6 位信息,每 3 字节被编成 4 个字符,所以合法长度一定是 4 的倍数:

长度 % 4补 = 个数是否合法举例
00合法示例载荷段 180 个字符
22合法补成 4 的倍数再解
31合法示例签名段 43 个字符
13非法直接报错,不再尝试

结果边界、时间换算与舍入说明

常见问题

解出来的内容能证明令牌是真的吗?

不能。头部与载荷只是 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 解释必然大量非法,所以页面只显示它的原样字符与字节数,不把它当文本解码。

相关工具