如何解码 JWT Token:开发者完整指南(含在线工具)
接口刚返回 401 Unauthorized。Authorization: Bearer eyJhbGciOi... 这一行看起来没问题。是 token 过期了?是 audience 写错了?还是签发方刚轮换了密钥?不把 token 里的内容读出来,这些问题一个都答不上。好消息是,解码一个 JWT 不需要密钥,不需要依赖库,也不需要联网。一个 JWT 就是三段 base64url 编码、用点号连接的字符串。解码过程很机械:拆分、base64url 解码、JSON.parse。没有魔法,也没有密码学。
本文会拆解 JWT 的结构,演示如何在 Node.js、Python、Go 和浏览器里解码 JWT,讲清楚让大多数团队翻车的「解码 vs 验证」之分,并列出你真的会踩到的失败模式。如果你现在就想看一下手头这个 token,直接跳到我们的 免费 JWT 解码器 ,它完全跑在浏览器里,生产 token 永远不会离开你的设备。
什么是 JWT?(结构速览)
JSON Web Token(JWT)是 RFC 7519 定义的一种紧凑、URL 安全的凭证格式。它用于在两方之间传递声明(claims),也就是关于用户和 token 本身的数据。一个 JWT 是三段 base64url 编码的内容,用点号连接:头部(header)、有效载荷(payload)、签名(signature)。
下面是一个真实的 token 拆成三段的样子:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ← header
.
eyJzdWIiOiJ1c2VyXzEyMyIsImV4cCI6MTk5OTk5OTk5OX0 ← payload
.
4NhxPjwoZxPNuxG-2C5ugGxaUsUJ0QyskAz7Ymz5Sg0 ← signature
header 描述 token 的签名方式,通常是 { "alg": "HS256", "typ": "JWT" }。payload 承载声明(claims),既有 RFC 注册的标准字段如 sub、exp、iat,也有自定义字段如 role 或 tenant。signature 是基于 header 和 payload 算出来的密码学凭证,用于让接收方检测内容是否被篡改。Base64url 是 base64 的 URL 安全变体;想花十分钟快速入门,可以看我们的 Base64 新手入门 。
现代身份认证体系里到处都是 JWT:OAuth 2.0 的 access token、OpenID Connect 的 ID token,Auth0、Okta、Clerk、Supabase、Firebase 签发的 API 凭证,服务网格里微服务之间传递的 token。过去十年里它基本就是默认的凭证格式。
继续往下之前,有一句话必须刻在脑子里:JWT 是编码的,不是加密的。拿到 token 的任何人都能读出里面的每一个声明。签名只证明来源,不隐藏内容。这一条决定了后续的一切:payload 里能放什么、为什么解码不需要密钥、为什么服务端必须做签名校验。
JWT 解码是怎么工作的?(base64url,不是解密)
解码 JWT 不是密码学操作,就是四个机械步骤:
- 按
.把 token 拆成三段,不多不少。 - 对第一段做 base64url 解码并用 JSON 解析,这就是 header。
- 对第二段做 base64url 解码并用 JSON 解析,这就是 payload。
- 第三段(signature)保留原始字节,要验证它必须用密钥。
整个算法就这些。不需要任何库。任何有 base64 和 JSON 解析器的语言,五行代码就能解码 JWT。想亲手感受一下 2 和 3 两步,可以用我们的 Base64 编码解码 工具自己跑一遍。
什么是 base64url?
Base64url 就是普通 base64 做了三处微调,让输出在 URL 和 HTTP 头里也安全:- 替代 +、_ 替代 /、末尾的 = 填充被丢掉。如果你直接把 base64url 塞进标准 base64 解码器而不还原这些替换,得到的要么是乱码,要么是报错。我们的 Base64 高级指南 详细讲了填充相关的边界情况。
| 标准 base64 | base64url | |
|---|---|---|
| 字母表 | A-Z a-z 0-9 + / | A-Z a-z 0-9 - _ |
| 填充 | 末尾必须 = | 丢弃 |
| URL 安全? | 否 | 是 |
| 示例 | PDw/Pz8+ | PDw_Pz8- |
另外一点要单独说:你无法在客户端「解密」签名。解码是从编码字节到 JSON 的单向过程。验证签名是另一回事,需要 HMAC 密钥(HS 系列算法)或签发方的公钥(RS、PS、ES、EdDSA)。
为什么不需要密钥就能解码
因为 payload 是 base64url + JSON,不是密文。只有当你想证明 token 没被篡改时,也就是做签名校验时,密钥才会登场。链路上任何能拿到 token 的人、日志里捞到 token 的人、打开浏览器的人,都能读到你放进去的每一个声明。所以绝对不要在 JWT payload 里放密码、API key,或者接收方本来不知道的 PII。更完整的威胁模型请看我们的 Web 开发者安全最佳实践 。
3 步在线解码 JWT,免费 JWT 解码器
有时候你就是需要一个立刻能看的答案:这个 token 过期了吗?aud 是不是我以为的那个?header 里会不会写着 alg:none?最快的路径是我们的在线 JWT 解码器。它就是为凌晨两点的事故排查而生的。
- 粘贴 完整的 token 到输入框,三段都要带上。
- 阅读 解码后的 header、payload,以及顶部的状态标签:算法、签发时间、过期时间,如果
exp已过,会有红色的Expired标签。 - 复制 任意一块需要的内容到 bug 报告、Slack 或测试 fixture 里。
为什么生产 token 也能安全粘贴:
- 100% 浏览器本地。解码调用原生
atob和JSON.parse,任何时候都不发网络请求。 - 无日志、无追踪、无 cookie、无需注册。
- 页面加载后可离线使用。
JWT 解码器对算法无感知。因为解码只需要 base64url 和 JSON,它能读取任何 JWS 变体:HS256/384/512、RS256/384/512、PS256/384/512、ES256/384/512、EdDSA,以及 alg:none。只有签名验证才依赖算法,而签名验证不适合交给公开的 Web 工具,稍后会讲到原因。
想手工 base64 解一段来交叉校验工具的结果?用 Base64 编码解码 ,把每段当作 base64url 输入。
用代码解码 JWT(Node.js、Python、Go、浏览器)
交互式调试之外的一切场景(中间件、测试、迁移脚本、CLI 工具)都会用到库。下面给出四种最常见运行环境里解码 JWT 的最小代码,同时并排展示「只读」路径和「验证」路径。每段代码都可以直接复制运行,输出与注释一致。
Node.js 解码 JWT(jsonwebtoken)
// npm install jsonwebtoken
const jwt = require('jsonwebtoken');
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9' +
'.eyJzdWIiOiJ1c2VyXzEyMyIsImV4cCI6MTk5OTk5OTk5OX0' +
'.4NhxPjwoZxPNuxG-2C5ugGxaUsUJ0QyskAz7Ymz5Sg0';
// 仅解码 —— 不会校验签名
const decoded = jwt.decode(token, { complete: true });
console.log(decoded.header); // { alg: 'HS256', typ: 'JWT' }
console.log(decoded.payload); // { sub: 'user_123', exp: 1999999999 }
// 验证 —— 生产环境路径
const secret = process.env.JWT_SECRET;
const verified = jwt.verify(token, secret, { algorithms: ['HS256'] });
调用 verify 时一定要显式传 algorithms 白名单。历史上不传这个参数曾让攻击者把 RS256 token 「降级」成 HS256,用公钥当 HMAC 密钥签名,这就是经典的算法混淆攻击(algorithm confusion)。白名单就是你的防线。
Python 解码 JWT(PyJWT)
# pip install PyJWT
import jwt
token = (
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
".eyJzdWIiOiJ1c2VyXzEyMyIsImV4cCI6MTk5OTk5OTk5OX0"
".4NhxPjwoZxPNuxG-2C5ugGxaUsUJ0QyskAz7Ymz5Sg0"
)
# 仅解码 —— 不能用于认证,适合检查 token 内容
decoded = jwt.decode(token, options={"verify_signature": False})
print(decoded) # {'sub': 'user_123', 'exp': 1999999999}
# 不动 payload 只读 header
header = jwt.get_unverified_header(token)
print(header) # {'alg': 'HS256', 'typ': 'JWT'}
# 验证 —— 生产环境路径
payload = jwt.decode(
token,
key="your-hs256-secret",
algorithms=["HS256"],
audience="api.example.com",
)
PyJWT 不传 algorithms 列表就拒绝验证。这个默认行为很合理,能堵上和 Node 例子里同类的混淆攻击。
Go 解码 JWT(golang-jwt/jwt/v5)
// go get github.com/golang-jwt/jwt/v5
package main
import (
"fmt"
"github.com/golang-jwt/jwt/v5"
)
func main() {
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" +
".eyJzdWIiOiJ1c2VyXzEyMyIsImV4cCI6MTk5OTk5OTk5OX0" +
".4NhxPjwoZxPNuxG-2C5ugGxaUsUJ0QyskAz7Ymz5Sg0"
// 仅解码
parser := jwt.NewParser()
claims := jwt.MapClaims{}
_, _, err := parser.ParseUnverified(tokenString, claims)
if err != nil {
panic(err)
}
fmt.Println(claims["sub"], claims["exp"]) // user_123 1.999999999e+09
// 验证
secret := []byte("your-hs256-secret")
token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected alg: %v", t.Header["alg"])
}
return secret, nil
})
fmt.Println(token.Valid, err)
}
keyFunc 闭包就是你强制算法族的地方:在返回密钥之前,先拒绝所有不符合预期的签名方法。
浏览器里解码 JWT(零依赖)
有时候你根本不想引任何依赖,比如临时调试面板、浏览器扩展、展示当前用户角色的小 UI 徽章。浏览器原生 API 就够了:
function decodeJwt(token) {
const [h, p] = token.split('.');
const pad = (s) => s + '==='.slice((s.length + 3) % 4);
const decodeSegment = (s) => {
const b64 = pad(s).replace(/-/g, '+').replace(/_/g, '/');
const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
return JSON.parse(new TextDecoder().decode(bytes));
};
return { header: decodeSegment(h), payload: decodeSegment(p) };
}
const { header, payload } = decodeJwt(token);
console.log(header); // { alg: 'HS256', typ: 'JWT' }
console.log(payload); // { sub: 'user_123', exp: 1999999999 }
TextDecoder 这一步对任何 payload 含非 ASCII 字符的 token 都很关键(昵称里的 emoji、preferred_username 里的西里尔字母):裸 atob 返回的是二进制字符串,多字节 UTF-8 会让 JSON.parse 炸掉。这正是我们的 在线 JWT 解码器 在你浏览器里跑的逻辑,只是多了一层 UI。
对比表
| 语言 | 仅解码 | 验证 | 库 |
|---|---|---|---|
| Node.js | jwt.decode(token) | jwt.verify(token, key, { algorithms: [...] }) | jsonwebtoken |
| Python | jwt.decode(token, options={"verify_signature": False}) | jwt.decode(token, key, algorithms=[...]) | PyJWT |
| Go | parser.ParseUnverified(token, claims) | jwt.Parse(token, keyFunc) | golang-jwt/jwt/v5 |
| 浏览器 | atob + TextDecoder + JSON.parse | 交给后端 | — |
解码 vs 验证:决定性差别
解码 JWT 是读它的声明;验证 JWT 是证明这些声明没被篡改过。解码永远不信任;验证才是信任本身。这个差别就是一个能用的认证实现和一个 CVE 之间的分界线。
| 解码 | 验证 | |
|---|---|---|
| 需要密钥? | 否 | 是 |
| 可以跑在客户端? | 可以(安全) | 绝不可以 |
| 证明真实性? | 否 | 是 |
| 检查过期? | 可选 | 是 |
| 使用场景 | 调试、检查 | 认证、授权 |
绝不要基于「已解码但未验证」的 JWT 做授权决策。 中间件里不行,React hook 里不行,你以为躲在网关后面的 serverless 函数里也不行。已解码的声明只代表 token 自称是什么;已验证的声明才代表签发方实际签了什么。攻击者可以手工构造一个签名无效的 token 塞给你的服务器,payload 想写什么都行,只有签名校验能挡住他。
HMAC 系列还有一个关键细节:如果你用 HS256,密钥的熵就是全部。短而可猜的密钥会被攻击者拿到任意一个 token 后离线爆破,一旦破出来,他就能自己签发 token 从正门走进来。请至少用 256 位真随机数。想看为什么这个数字很重要,请读 密码熵详解 。
常见 JWT 声明速查
你遇到的每一个 JWT 都用到 RFC 7519 注册声明的某个子集。这张短表值得背下来:
| 声明 | 名称 | 示例 | 备注 |
|---|---|---|---|
iss | Issuer(签发方) | https://auth.example.com | 谁签发了这个 token |
sub | Subject(主体) | user_123 | 通常是用户 ID |
aud | Audience(受众) | api.example.com | token 给谁用,服务端必须校验 |
exp | Expiration(过期) | 1715003600 | Unix 秒;过期即失效 |
iat | Issued At(签发时间) | 1715000000 | Unix 秒,token 的签发时刻 |
nbf | Not Before(生效时间) | 1715000060 | token 最早可用的时刻 |
jti | JWT ID | d1f8… | 每个 token 唯一;防重放 |
kid | Key ID(在 header 里) | key-2025-01 | JWKS 里哪个密钥签的这个 token |
业务自定义声明会和上面这些并排出现:role、scope、email、tenant_id,你的 IDP 想塞什么就塞什么。请保持短小,每一个字节都会跟着每一个请求走。
想把 iat 和 exp 读成人类可读的时间,试试我们的 Unix 时间戳转换器 。粘一个数字进去,立刻看到本地时区的日期,时钟漂移 bug 一秒就能发现。
排错:我的 JWT 为什么解不开?
按出现频率大致排序,五种真实的失败模式。每一种都写成 症状 → 原因 → 解决。
- 「JWT 格式无效,期望三段」。 你只复制了 payload,或者终端把 token 换行了,你只抓到了第一行。解决:从原始响应体里重新复制完整的
xxx.yyy.zzz,别从渲染后的终端复制。长串单行内容在浏览器 devtools 的 Network 面板里比在翻屏过的终端里更安全。 - 五段而不是三段。 你拿到的是JWE(加密 JWT),不是 JWS。格式是
header.encryptedKey.iv.ciphertext.tag。解码器能读到 header,但 payload 是密文。解决:要解密 payload 必须有解密密钥,通常这事由你的认证 SDK 在服务端处理,不是调试工具的活。 - token 看起来合法,却在 base64url 这一步报错。 token 在复制链路里被 URL 编码过(cookie、重定向 URL、抓包代理日志)。你会看到里面出现字面量
%2E或%2B。解决:先 URL 解码一次 ,再把结果喂给 JWT 解码器。 - payload 的 JSON 解析失败。 终端或聊天客户端插了软换行,或者脚本把标识符两边换成了花引号(smart quotes)。解决:用 curl 的
-o file.txt或 devtools 的 Raw view 看原始响应字节,去掉空白,再粘一次。 - 能干净解码,但后端仍然拒绝。 这不是解码问题,是验证问题。token 结构合法,但服务端检查的某一项(签名、
aud、exp、时钟漂移、kid查表)失败了。跳到下一节。
两个虽然不是解析错误、但打开解码器时顺便要看的:header 里 alg 是 none(生产环境一律视为恶意),以及 exp 已在过去(解码器仍会显示声明方便你调试,这是正确行为,我们的工具会用红色 Expired 标签标出来)。
只解码不够时:签名验证
解码止步于「这个 token 声称了什么」。验证才是把「声称」转成「信任决策」的那一步。签名是用签发方私钥或共享密钥算出来的证明,把 header 和 payload 绑在一起:改一个字节,签名校验就失败。少了这一步,任何能往你接口发 POST 的人都能手搓一个「admin」token,改 payload、不带签名。
永远不要接受 alg:none。
无论语言和框架,生产级验证大致像下面这个清单。缺项请当 bug 修:
- 显式传
algorithms: ['RS256'](或你实际用的算法)白名单。这一条能干掉算法混淆攻击。 - 校验
aud等于你服务的标识,iss等于你期望的签发方 URL。 - 校验
exp对比当前时间,时钟漂移容忍度不超过 60 秒。 - 密钥轮换时,按
kid从 JWKS 端点查公钥,绝不要把一个密钥硬编码到永远。 - 撤销要做到有效:把
exp做短(分钟级,不是天级),并对高价值 token 额外维护jti黑名单。
所有主流 JWT 库都在一次调用里暴露了这些选项。如果你的验证代码没有显式设置它们,你就是在用默认值,而默认值历来就是 bug 的根源。完整威胁模型请看我们的 Web 开发者安全最佳实践 。
FAQ
不知道密钥能解码 JWT 吗?
可以。header 和 payload 是 base64url 编码,不是加密,拿到 token 的任何人都能读到声明。只有验证签名才需要密钥或公钥。这是设计使然:payload 本来就是给接收方读的,才能据此做授权决策。
在线解码器里粘贴生产 JWT 安全吗?
只有在解码器完全跑在你浏览器里、永不上传 token 时才安全。我们的 JWT 解码器 在本地用原生 atob 和 JSON.parse 解析,任何服务器都不会收到 token。那些把你的 token POST 到 API 的远程调试器应当视同凭证泄露。
JWT 解码和验证有什么区别?
解码只是读声明,不需要密钥,也证明不了任何事。验证会拿签发方的密钥检查签名,确认 token 没被篡改。绝不要基于已解码但未验证的 token 做认证决策。
我的 JWT 看起来被截断了,合法格式是什么样?
合法的 JWT 就是三段用点号分隔的 base64url:header.payload.signature。五段说明你拿到的是加密的 JWE,不是 JWS。没有点号则说明你只从终端换行后的第一行复制了一段。
为什么解码器还显示过期的 token?
解码器会无视有效性把声明读出来,方便你调试被拒的原因。只有验证器才会拒绝过期 token。我们的工具会把 exp 和你本地时钟对比,并显示一个 Expired 标签,你不用盯着 Unix 时间戳眯眼算。
能解码哪些算法?
全都能。解码只需要 base64url 和 JSON 解析,对算法无感。包括 HS256/384/512、RS256/384/512、PS256/384/512、ES256/384/512、EdDSA,以及 alg:none。只有验证才依赖你选的算法。
Node.js 里该用 jwt-decode 还是 jsonwebtoken?
前端只需要读 payload 时用 jwt-decode,比如从 access token 里取用户名显示。后端用 jsonwebtoken,因为只有后端能保管签名密钥并执行 jwt.verify。千万不要在客户端做验证。
结语
解码 JWT 远没有「密码学 token」听起来那么神秘。记住下面五条,你再也不会对着不透明的 eyJhbGciOi… 字符串发呆:
- 解码就是 base64url + JSON 解析,不需要密钥。
- JWT 有三段(header、payload、signature),用点号连接。
- 解码永远不证明真实性。生产路径必须在服务端用签发方密钥验证。
- 拒绝
alg:none,调用verify时显式传算法白名单。 - 不要在 payload 里放密码、私钥或敏感 PII,任何拿到 token 的人都能读到。
把我们的 免费 JWT 解码器 加个书签,on-call 调试时可直接用。粘一个 token,读出声明,一秒看出过期状态,全程 token 不离开浏览器。