杂记 › 技术笔记 › JWT 那三段字符串里到底写了什么?header、payload 与 exp 的真实读法

JWT 那三段字符串里到底写了什么?header、payload 与 exp 的真实读法

上个月替同事看一个内部系统的登录接口。前端拿到 401,抓包里 Authorization 头是 Bearer 加一长串、用 . 分成三节的字符,肉眼扫不出可读的词。他要的只有两件事:这串东西什么时候到期,role 里是不是 admin。我先把这两问拆开:「读得到什么」和「服务端认不认」是两条线,混在一起就排查不下去。

它是「编码 + 签名」,不是加密

前两节是 base64url 编码,不要密钥谁都能还原成 JSON;第三节才是签名,只有拿到对称密钥(HS 那一族)或对应公钥(RS 那一族)才验得了。这两层常被压成一句「加密了所以看不见」,其实相反:你看不见只因为还没解码,拿到串的人能看到全部字段。样例留下的三节长度并排抄出来:

Header    : eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Payload   : 264 个 base64url 字符
Signature : 43 个 base64url 字符

三段对照:字符数、字节数、能不能读

段base64url 字符数解出后能不能读出内容本批样例里的值
Header3627 字节 / 27 字符能,不需要密钥{"alg":"HS256","typ":"JWT"}
Payload264198 字节 / 194 字符能,但读出文本不等于可信iss、sub、aud、name、role、vip、iat、nbf、exp、jti
Signature4332 字节 / 30 字符读不出意图样例密钥 your-256-bit-secret

载荷那行 198 字节与 194 字符不是同一个数:name 的值是中文「张三」,UTF-8 下中文占的字节比字符多,role 则解出 ["admin","editor"]。手写 base64url 解码字符表一加就错位,这步我交给 JWT 解析工具:三段各报字节数与字符数,逐条解释注册声明,解析和验签都在浏览器里完成,页面不发网络请求。

iat、nbf、exp 都是 UTC 秒

  • 有效样例的 iat 与 nbf 都是 1792402758,换成 2026-10-19 09:39 UTC;两值相等,签发即生效。
  • 同一段的 exp 是 1861919999,等于 2028-12-31 23:59 UTC,相对脚本运行那次还剩 802.8944907407407 天。
  • 已过期样例 iat 1748593800 对应 2025-05-30 08:30 UTC,exp 1748766600 对应 2025-06-01 08:30 UTC。
  • 那段的 exp 相对脚本运行时刻已过去 506.7513310185185 天,签名却依然是好的。

换算时别把秒当毫秒喂给日期函数;同一秒会给出 UTC 和本地两行,先确认读的是哪一行。想还原某一节内容可以用 Base64 编解码,把秒数换成可读日期用 时间戳转换。

验签通过和没过期是两件事

已过期的那段样例,签名段仍是 32 字节,用样例密钥 your-256-bit-secret 重算 HMAC,验签照样写「通过」。签名锁的是「签发到现在,前两节没被改过一个字符」,不含时间语义;服务端只验签不查 exp,很久前签的令牌照样能用。iss、aud 同理要服务端自己比对,工具不会替你查,权限判断更不在它职责里。

三种边界,长度先出卖了它们

样例algkid签名段字节数那次运行打印的验签结果
有效HS256无32通过(secret = your-256-bit-secret)
已过期HS256无32通过,但 exp 落在 2025-06-01 08:30 UTC
alg=nonenone无0没有签名可验
载荷不是 JSONHS256无32不匹配
RS256RS256demo-key-1256通过(用页面内置公钥)

alg=none 那行最值得盯:整串仍切成三节,最后一节解出 0 字节,等于没签名。风险在老的服务端库按令牌自报的算法验签——载荷改掉、算法写 none、签名抹空,校验就被绕过。载荷不是 JSON 的那种能解出原文「这是一段不是 JSON 的载荷文本」,但结构不成,验签报不匹配,脚本在那行后面直接打出重算的签名 4prUH45MQAmbRVmdqHYpEsc2OzZ5OM0TEgApoZOXywI,跟令牌自带那节一比就看出谁动过。

对称密钥的强度是能算出来的

HS 族同一把密钥签发和验证,密钥长度决定它扛不扛得住离线爆破。样例那把 your-256-bit-secret 只是演示占位:26 个小写字母取 8 位,候选空间 208827064576 种,每秒试 100 万次,58.01 小时扫完;字符表扩到大小写加数字 62 个、取 12 位,空间 3.2262667623979e+21 种,每秒 10 亿次也要 102421.2 年。前者是一个周末的量级,后者才勉强能指望。

几个当时没搞干净的问题

Q:这串字符是不是加密过的?谁能看到里面的内容?

没加密。前两节只是 base64url 编码,拿到的人都能还原成 JSON,它防篡改不防偷看。手机号、密码、证件号应该只在载荷里留 ID、服务端再查;键名撞上这些词时页面会提示,但拦住已签出的令牌不是它做的事。

Q:base64 和 base64url 差在哪?

字符表不同:base64url 把 + 和 / 换成 - 和 _,结尾补位符 = 也不写,一整节才能塞进 URL 和 header。常见示例载荷里那段 JSON 是 55 个字符,编成 base64url 后是原来的 1.345 倍。

Q:过期但验签通过的令牌,服务端到底认不认?

取决于服务端查不查。已过期那条就是典型组合:重算一致、验签通过,exp 却是 2025-06-01 08:30 UTC。有效期、签发者、受众、是否吊销各是各的检查项,漏一项就可能放行。

Q:HS256 和 RS256 该选哪个?

自己签自己验,HS 族省事,代价是每个验证方都握着同一把密钥;公钥要发给多个验证方时用 RS 或 ES 族,避免共享密钥外泄,代价是前面那 256 字节的签名。

回到同事那两个问题。到期时间是从 exp 那行 UTC 读出来的,不是从报错码猜的;role 是不是 admin,读出来只说明签发方当时这么写,接口认不认还得服务端说了算。我现在的取舍是载荷只放 ID 和一组角色名——多一个字段多一处泄露面,签名能防的只有篡改,不是偷看。

📎 相关文章

sin(π) 算出 0.0548 而不是 0:角度制、弧度制和计算器的优先级 2026-10-21 同一批投放链接为什么跑出两行数据?UTM 五个参数、大小写与一次百分号编码 2026-10-21 网址里哪一段会到服务器?六段拆分与 %20、+、二次编码 2026-10-19 身份证最后一位为什么是 X?前 17 位加权取模算出的校验位 2026-10-19 子网掩码写成 /22 能带多少台设备?主机位、网络地址与广播地址 2026-10-19