接口返回一个几百字符的 eyJhbGciOi... 字符串,你想知道里面装了什么、什么时候过期——jwt在线解析做的就是这件事:把三段 Base64URL 还原成 JSON,直接告诉你算法、签发时间、过期时间和当前状态。
但有两条底线必须先记住:payload 只是编码,不是加密,任何人都能解出来;签名没有密钥就验不了,在线工具告诉你的只是「它写了什么」,不是「它对不对」。
一、三段结构:header.payload.signature
一个 JWT 就是三个用英文句点隔开的字符串,每一段各自独立做 Base64URL 编码:
| 段 | 里面是什么 | 编码 | 能不能直接读懂 |
|---|---|---|---|
| Header | 算法 alg、类型 typ 等头部参数 |
Base64URL(JSON) | 能,就是一段 JSON |
| Payload | 各种声明(claims) | Base64URL(JSON) | 能,就是一段 JSON |
| Signature | 对前两段做的签名 | Base64URL(二进制字节) | 不能,它是一串摘要字节 |
这里最容易和 Base64 混淆的一点:JWT 用的是 Base64URL,不是标准 Base64。 区别在于标准 Base64 的字母表里第 62、63 位是 + 和 /,并且会在末尾补 = 把长度凑成 4 的倍数;而 JWT 要放进 URL 和 HTTP 头,+ 在查询串里会被解析成空格,/ 是路径分隔符,= 也容易惹麻烦,所以 RFC 4648 单独定义了 URL 安全变体:+ 换成 -,/ 换成 _,并且去掉所有 = 补位。
后果就是你把 JWT 的某一段直接粘进普通的 Base64 解码框,有时能出结果、有时报「非法字符」或「长度错误」——取决于那一段有没有出现 + / /,以及解码器对缺失 = 的容忍度。要么用专门的 JWT 解析,要么先做字符替换再补位,第七节有可直接复制的写法。
顺带一句:整串 JWT 不是一次 Base64,三段各自编码后用句点拼接,把整串拿去解码永远得不出东西。
二、标准声明速查表
payload 里的字段叫声明(claim),RFC 7519 定义了 7 个注册声明,它们都不是必填,但只要用了就必须按规范取值:
| 声明 | 全称 | 含义 | 取值与注意 |
|---|---|---|---|
iss |
Issuer | 签发者 | 字符串或 URL,用来确认 token 是谁发的 |
sub |
Subject | 主体,通常是用户 ID | 只放标识,别放手机号、邮箱 |
aud |
Audience | 受众,即目标服务 | 字符串或字符串数组;对不上就应该拒绝 |
exp |
Expiration Time | 过期时间 | 秒级 Unix 时间戳,不是毫秒 |
nbf |
Not Before | 最早生效时间 | 同样秒级;当前时间早于它时应拒绝 |
iat |
Issued At | 签发时间 | 同样秒级;可用来算 token 年龄 |
jti |
JWT ID | 本 token 的唯一 ID | 防重放、做吊销黑名单的钥匙 |
exp / nbf / iat 在规范里叫 NumericDate,单位统一是秒。JS 里 Date.now() 给的是毫秒,很多线上事故就出在少写了一个 / 1000:签发端把毫秒填进去,exp 会变成公元五万年,所有校验库的过期判断全部失效。反过来,校验端拿毫秒去比秒,结果就是每个 token 都被判为已过期。想知道某个数字对应哪一天,粘进时间戳转换工具看一眼最快——离谱到几万年的,基本就是单位错了。
业务字段可以随便加,但自定义名有冲突风险,规范做法是用 URI 做命名空间。
三、解析能看到什么,看不到什么
能看到的: header 和 payload 的全部明文、签名段的原始字符串、以及由 exp / nbf 推算出的状态。
看不到的: 签名的真伪(没有密钥算不出对比值)、这个 token 是否已被吊销、签发方是不是真的它声称的那个。
关键结论是:payload 没有加密,只是编码,解码过程和「解」一个 Base64 字符串没有任何区别。 任何拿到 token 的人——前端、网关、日志系统、CDN 缓存——都能在几毫秒内读出里面的每一个字段。因此下面这些东西绝不能放进 payload:
- 密码、密钥、session 内容
- 手机号、身份证号、银行卡号
- 详细住址、真实姓名等能定位到个人的信息
真正加密的 JWT 叫 JWE(RFC 7516),平时看到的 eyJ... 几乎都是只签名不加密的 JWS:header 的 alg 是 HS256/RS256/ES256 即为 JWS,出现 A128GCM、RSA-OAEP 这类才是 JWE。
只想看看里面写了什么,用工具最快:
本站这个工具会给出:算法 alg、类型 typ、签发时间、过期时间(按本地时区格式化)、一个状态标签(有效 / 已过期 / 尚未生效 / 不过期),以及三段的分色展示和 payload 的逐条声明列表。它不校验签名——签名区下面那句提示写得明白:验签需要密钥,本工具只做本地解码。所以别把生产环境的 token 粘进任何在线工具,纯前端实现虽然不上传,但 token 本身一旦泄露就是完整凭证。
四、过期判断与时钟偏移
校验规则只有两条:now > exp 拒绝,now < nbf 也拒绝。但真实世界的时钟不会完全一致,所以 RFC 7519 明确允许实现者留一点余量,这就是 leeway(时钟偏移容忍),在 jsonwebtoken 里叫 clockTolerance,常见取值 5~60 秒。没有它,一个在负载均衡节点间差了几秒的集群会随机拒绝刚签发的 token。
几个实践建议:
- 余量只留几十秒,别用「宽容一小时」来掩盖时钟问题,那是把安全窗口直接让出去
- payload 里没有
exp时,工具会显示「不过期」,这不是好事——永不过期的 token 等于一张永久凭证,签发端漏了exp属于必须修的缺陷
五、signature:没有密钥就验不了
签名的计算对象不是整个 token,而是 base64url(header) + "." + base64url(payload) 这一段字符串:HS256 用同一个密钥做 HMAC-SHA256;RS256 用私钥签名、公钥验签。验签就是拿密钥重算一遍再比对,所以没有密钥的在线工具永远只能解、不能验,这不是工具偷懒,是数学上做不到。
alg=none 的历史漏洞。 早期若干 JWT 库允许 header 里 alg 为 none,并据此跳过签名校验。攻击者于是把 payload 改成 {"sub":"admin"},把 alg 改成 none,删掉签名段,服务端照样放行。修复方式只有一个:服务端强制算法白名单,不按 token 里写的来。
RS256 与 HS256 的混淆攻击。 如果服务端照着 header 的 alg 去选算法,攻击者可以把 RS256 改成 HS256,然后拿公开的公钥当 HMAC 密钥自己签一个 token——公钥本来就是公开的,服务端若用公钥按 HMAC 验,居然会通过。结论很硬:算法必须由服务端配置决定,绝不能信任 header 里的 alg。
| 场景 | 建议算法 | 理由 |
|---|---|---|
| 单体应用、内部服务,密钥可安全共享 | HS256 | 计算快,实现简单 |
| 对外开放、签发方与校验方分离 | RS256 / ES256 | 校验方只持公钥,无法伪造 |
| 完全不签名 | none | 生产环境一律禁止 |
六、刷新 token,以及登出为什么「失效不了」
JWT 是无状态的:签发完之后服务端不保存任何「当前有效列表」,校验只看签名和 exp。这带来一个经典问题——用户点了登出,前端删掉 localStorage,但那个 token 在 exp 之前依然是有效的,被人截走照样能用。
常见做法是 access token 短(15~30 分钟)+ refresh token 长(数天):refresh token 存服务端、可查询、可吊销,登出时废掉它,access token 最多再活半小时。要更彻底还有三招:黑名单(登出时把 jti 写进 Redis,TTL 设为剩余 exp,校验时查一次)、版本号(用户表放 token_version,改密码或登出时 +1,payload 带上并比对)、refresh token 轮换(每次刷新都换新的并作废旧的那张,泄露能被发现)。
做得越多越「有状态」,也就越失去 JWT 轻量化的初衷,按业务对泄露的容忍度取舍即可。
七、命令与代码速查(可直接复制)
# Node 一行解出 payload(Node 15+ 支持 base64url)
TOKEN='eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.xxx'
node -e 'const p=process.argv[1].split(".")[1];console.log(JSON.stringify(JSON.parse(Buffer.from(p,"base64url").toString()),null,2))' "$TOKEN"
# 老版本 Node:手动做 Base64URL → Base64 的转换
node -e 'const p=process.argv[1].split(".")[1].replace(/-/g,"+").replace(/_/g,"/");console.log(Buffer.from(p,"base64").toString())' "$TOKEN"
# 只要看过期时间
node -e 'const c=JSON.parse(Buffer.from(process.argv[1].split(".")[1],"base64url").toString());console.log(new Date(c.exp*1000).toISOString(), Date.now()/1000>c.exp?"已过期":"有效")' "$TOKEN"
# Python:补 '=' 是关键,长度对 4 取模的余数决定补几个
import base64, json, time
def b64url_decode(seg: str) -> bytes:
return base64.urlsafe_b64decode(seg + '=' * (-len(seg) % 4))
def jwt_payload(token: str) -> dict:
return json.loads(b64url_decode(token.split('.')[1]).decode('utf-8'))
claims = jwt_payload(token)
print(claims['exp'], time.time() > claims['exp'])
// Go:RawURLEncoding 就是无补位版本,正好对应 Base64URL
parts := strings.Split(token, ".")
raw, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
log.Fatal("payload 不是合法 Base64URL: ", err)
}
var claims map[string]any
json.Unmarshal(raw, &claims)
// Java:getUrlDecoder() 对应 URL 安全字母表,通常无需手动补 '='
String[] parts = token.split("\\.");
byte[] raw = Base64.getUrlDecoder().decode(parts[1]);
String payload = new String(raw, StandardCharsets.UTF_8);
服务端校验请用成熟库并把算法白名单写死,别自己拼这些。
八、常见报错对照表
| 报错 / 现象 | 根因 | 怎么修 |
|---|---|---|
TokenExpiredError: jwt expired |
now > exp |
用 refresh token 换新;检查服务器时钟 |
JsonWebTokenError: invalid signature |
密钥不一致 / 算法不匹配 / payload 被改过 | 确认签发与校验用同一密钥同一算法 |
JsonWebTokenError: jwt malformed |
不是三段 | 检查有没有多复制、少复制、带 Bearer 前缀 |
NotBeforeError: jwt not active |
now < nbf |
多为签发端时钟偏快,或 nbf 填成了毫秒 |
invalid character / 解出乱码 |
用标准 Base64 去解 Base64URL | 先 -→+、_→/,再补 = 到 4 的倍数 |
| 中文全是问号或方块 | 签发端没有用 UTF-8 编码 payload | 规范约定是 UTF-8,统一编码后重签 |
| 工具提示「不是合法的 JWT」 | 三段数量不对,或前两段解出来不是 JSON | 见下面第九节清单 |
最后一行最常见的两个诱因:从 Authorization 头复制时带了 Bearer 前缀,以及被邮件客户端或 IDE 自动折行插进了换行。本站工具会自动去掉首尾空白,但中间的换行它救不了。
九、排错清单(按顺序过一遍)
- 去掉
Bearer前缀和首尾空白,确认是单行且恰好两个句点 - 确认字符集是 URL 安全的:只有
A-Z a-z 0-9 - _ .,出现+/说明被标准 Base64 处理过 - 用工具先看 header 的
alg,确认它和你服务端配置的算法一致 - 看
exp换算出来的日期,判断是不是毫秒单位填错 - 校验失败时先确认密钥来源(KMS / 环境变量 / 配置中心),再看算法
- 状态显示「不过期」的,回签发端补
exp - 生产 token 一律不要粘到任何在线工具,用本地命令行解
十、速查表
结构 base64url(header).base64url(payload).base64url(signature)
编码 + → - / → _ 去掉 = 补位(RFC 4648 §5)
exp/iat/nbf 单位 = 秒(NumericDate),不是毫秒
过期判断 now > exp 拒绝;now < nbf 拒绝;留 5~60 秒 leeway
安全 算法由服务端白名单决定,不信 header 的 alg;alg=none 一律拒绝
payload 只是编码不是加密,禁止放密码 / 手机号 / 身份证
验签 没有密钥就验不了,在线工具只能解不能验
相关工具:JWT 解析|Base64 编码解码|时间戳转换
全部纯前端实现,输入的内容不上传服务器。