jwt在线解析:三段结构、exp 过期判断与常见报错
接口返回一个几百字符的 eyJhbGciOi... 字符串,你想知道里面装了什么、什么时候过期——jwt在线解析做的就是这件事:把三段 Base64URL 还原成 JSON,直接告诉你算法、签发时间、过期时间和当前状态。
但两条底线必须先记住:payload 只是编码,不是加密,任何人都能解出来;签名没有密钥就验不了。在线工具告诉你的只是「它写了什么」,不是「它对不对」。
一、三段结构:header.payload.signature
一个 JWT 就是三个用英文句点隔开的字符串,每一段各自独立做 Base64URL 编码:
| 段 | 里面是什么 | 能不能直接读懂 |
|---|---|---|
| Header | 算法 alg、类型 typ 等头部参数 |
能,就是一段 JSON |
| Payload | 各种声明(claims) | 能,就是一段 JSON |
| Signature | 对前两段做的签名 | 不能,它是一串摘要字节 |
最容易和 Base64 混淆的一点:JWT 用的是 Base64URL,不是标准 Base64。 标准 Base64 的字母表第 62、63 位是 + 和 /,末尾还会补 = 凑长度;而 JWT 要放进 URL 和 HTTP 头,+ 在查询串里会被解析成空格,/ 是路径分隔符,所以 RFC 4648 单独定义了 URL 安全变体:+ 换成 -,/ 换成 _,去掉所有 = 补位。
后果就是你把 JWT 某一段直接粘进普通 Base64 解码框,有时能出结果、有时报「非法字符」或「长度错误」。另外,整串 JWT 不是一次 Base64,三段各自编码后用句点拼接,把整串拿去解码永远得不出东西。
二、注册声明速查表
| 声明 | 含义 | 取值与注意 |
|---|---|---|
iss |
签发者 | 确认 token 是谁发的 |
sub |
主体,通常是用户 ID | 只放标识,别放手机号、邮箱 |
aud |
受众,即目标服务 | 字符串或数组,对不上应拒绝 |
exp |
过期时间 | 秒级 Unix 时间戳,不是毫秒 |
nbf |
最早生效时间 | 秒级,当前时间早于它应拒绝 |
iat |
签发时间 | 秒级,可用来算 token 年龄 |
jti |
本 token 唯一 ID | 防重放、做吊销黑名单的钥匙 |
exp/nbf/iat 在规范里叫 NumericDate,单位统一是秒。JS 里 Date.now() 给的是毫秒,很多线上事故就出在少写了一个 / 1000:签发端把毫秒填进去,exp 会变成公元五万年,所有过期判断失效;反过来校验端拿毫秒比秒,每个 token 都被判为已过期。
三、payload 没有加密,别往里放敏感信息
解码过程和「解」一个 Base64 字符串没有任何区别。任何拿到 token 的人——前端、网关、日志系统、CDN——都能在几毫秒内读出每一个字段。因此这些绝不能放进 payload:密码与密钥、手机号与身份证号、银行卡号、能定位到个人的详细信息。
真正加密的 JWT 叫 JWE(RFC 7516),平时看到的 eyJ... 几乎都是只签名不加密的 JWS:header 的 alg 是 HS256/RS256 即为 JWS,出现 A128GCM、RSA-OAEP 才是 JWE。
想看看里面写了什么,用工具最快:
本站这个工具会给出算法、类型、签发时间、过期时间(按本地时区格式化)、状态标签(有效/已过期/尚未生效/不过期),以及三段分色展示和 payload 逐条声明列表。它不校验签名——验签需要密钥,本工具只做本地解码。所以生产环境的 token 别往任何在线工具里粘。
四、没有密钥就验不了签名,alg=none 是历史漏洞
签名的计算对象是 base64url(header) + "." + base64url(payload) 这一段:HS256 用同一密钥做 HMAC-SHA256,RS256 用私钥签名、公钥验签。所以没有密钥的在线工具永远只能解、不能验,这不是偷懒,是数学上做不到。
早期若干 JWT 库允许 header 里 alg 为 none 并据此跳过签名校验,攻击者把 payload 改成 {"sub":"admin"}、把 alg 改成 none、删掉签名段,服务端照样放行。修复方式只有一个:服务端强制算法白名单,不按 token 里写的来。同理,RS256 改成 HS256 拿公开公钥当 HMAC 密钥的混淆攻击,结论也是同一条:算法必须由服务端配置决定,绝不能信任 header 里的 alg。
五、常见报错对照表
| 报错 / 现象 | 根因 | 怎么修 |
|---|---|---|
TokenExpiredError: jwt expired |
now > exp |
用 refresh token 换新;检查服务器时钟 |
JsonWebTokenError: invalid signature |
密钥不一致 / 算法不匹配 | 确认签发与校验同一密钥同一算法 |
JsonWebTokenError: jwt malformed |
不是三段 | 检查有没有带 Bearer 前缀、多复制或少复制 |
NotBeforeError: jwt not active |
now < nbf |
签发端时钟偏快,或 nbf 填成了毫秒 |
invalid character / 解出乱码 |
用标准 Base64 去解 Base64URL | 先 -→+、_→/,再补 = 到 4 的倍数 |
| 中文全是问号或方块 | 签发端没用 UTF-8 编码 payload | 统一 UTF-8 后重签 |
最常见的两个诱因:从 Authorization 头复制时带了 Bearer 前缀,以及被邮件客户端或 IDE 自动折行插进了换行。
六、排错清单(按顺序过一遍)
- 去掉
Bearer前缀和首尾空白,确认是单行且恰好两个句点 - 确认字符集 URL 安全:只有
A-Z a-z 0-9 - _ .,出现+/说明被标准 Base64 处理过 - 先看 header 的
alg,确认它和服务端配置的算法一致 - 看
exp换算出来的日期,判断是不是毫秒单位填错 - 校验失败先确认密钥来源,再看算法
- 状态显示「不过期」的,回签发端补
exp - 生产 token 一律不要粘到在线工具,用本地命令行解
# Node 一行解出 payload(Node 15+ 支持 base64url)
node -e 'const p=process.argv[1].split(".")[1];console.log(JSON.stringify(JSON.parse(Buffer.from(p,"base64url").toString()),null,2))' "$TOKEN"
# Python:补 '=' 是关键
import base64, json
def jwt_payload(token):
seg = token.split('.')[1]
return json.loads(base64.urlsafe_b64decode(seg + '=' * (-len(seg) % 4)).decode('utf-8'))
七、一句话速查
结构 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 只是编码不是加密,禁止放密码 / 手机号 / 身份证
验签 没有密钥就验不了,在线工具只能解不能验
本文是精简版,完整版(含刷新 token 与登出失效的四种方案、Go/Java 解法、更多排错案例)见:jwt在线解析:三段结构、exp 过期判断与常见报错
相关工具:JWT 解析|Base64 编码解码|时间戳转换
浙公网安备 33010602011771号