3、令牌结构:JWT的Header、Payload、Signature详解

JWT 长什么样?说白了就是三段 Base64 编码的字符串,用点号连起来。我刚开始接触时也觉得这玩意儿挺简单,直到在生产环境踩了坑,才发现每一段都有门道。

一个典型的 JWT 长这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

三部分分别是 Header、Payload、Signature。咱们一个一个拆开看。

核心要点:JWT 的三段结构决定了它的安全边界。Header 管算法,Payload 管数据,Signature 管完整性。任何一段被篡改,签名校验都会失败。

3.1 Header:算法的声明

Header 是 JWT 的第一段。它告诉接收方:我用什么算法签的名,令牌是什么类型。

一个典型的 Header 长这样:

{
  "alg": "HS256",
  "typ": "JWT"
}
  • alg:签名算法。常见的有 HS256(HMAC-SHA256)、RS256(RSA-SHA256)、ES256(ECDSA-SHA256)。
  • typ:令牌类型。固定为 "JWT"。

嗯,这里要注意。alg 字段是攻击者的重点目标。我曾经在渗透测试中遇到过一种攻击,叫「alg=none」。攻击者把 alg 改成 "none",然后直接去掉签名部分。有些实现不严谨的库,居然就放行了。

避坑指南:服务端必须校验 alg 字段,拒绝 "none" 算法。我建议在代码里白名单化允许的算法列表,不在白名单里的直接拒绝。

另外,Header 里还可以放一些扩展字段。比如 jku(JWK Set URL)、jwk(公钥)、kid(密钥 ID)。这些在分布式场景下很有用,但也引入了额外的安全风险。我的建议是:除非你真的需要动态密钥轮换,否则别用 jku/jwk,直接用 kid 查本地缓存就够了。

3.2 Payload:数据的载体

Payload 是 JWT 的第二段。它存放实际的数据,也就是 Claims(声明)。

Claims 分三类:

类型 说明 示例
Registered Claims 预定义的标准化字段 iss, sub, exp, iat, nbf, jti
Public Claims 公开定义的字段,需在 IANA 注册 name, email, role
Private Claims 自定义字段,双方约定即可 department, tenant_id

一个典型的 Payload 示例:

{
  "iss": "https://auth.example.com",
  "sub": "user_123456",
  "aud": "api.example.com",
  "exp": 1735689600,
  "iat": 1735603200,
  "nbf": 1735603200,
  "jti": "unique--id-001",
  "name": "张三",
  "role": "admin",
  "tenant_id": "tenant_abc"
}

这里有几个字段,我个人觉得特别重要:

  • exp(过期时间):必须校验。我见过不少系统没校验 exp,导致令牌永久有效。
  • nbf(生效时间):防止令牌提前使用。适合预签发场景。
  • jti(令牌唯一标识):用于防重放攻击。配合黑名单使用效果更好。
  • iat(签发时间):审计用,也能辅助判断令牌是否被提前使用。

经验之谈:Payload 是 Base64 编码,不是加密。任何拿到令牌的人都能解码看到里面的内容。所以,不要把密码、信用卡号等敏感信息放在 Payload 里。我习惯的做法是:只放用户 ID 和角色这类非敏感标识,其他数据通过 API 查询。

3.3 Signature:完整性的保证

Signature 是 JWT 的第三段。它保证了令牌在传输过程中没有被篡改。

签名过程其实很简单:

signature = HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

接收方拿到令牌后,用同样的算法和密钥重新计算签名。如果计算结果和令牌里的签名一致,说明数据没被改过。

这里有个关键点:签名只保证完整性,不保证机密性。数据还是明文的,只是防止被篡改。如果你需要加密,得用 JWE(JSON Web Encryption),那是另一个话题了。

避坑指南:我曾经遇到过一个问题:服务端用 RS256(非对称算法)签名,但客户端用 HS256(对称算法)去验证。攻击者可以拿到公钥,然后用 HS256 伪造令牌。解决方案很简单:服务端必须校验 alg 字段是否和预期一致。

3.4 三者的协作关系

Header、Payload、Signature 三者缺一不可。它们的关系可以用一张图来表示:

JWT 三段结构协作关系 Header 算法声明 + 类型 Payload 数据声明(Claims) Signature 完整性校验 签名生成过程 Signature = HMAC( base64(Header) + "." + base64(Payload), Secret ) 校验流程 重新计算签名 → 与令牌中的 Signature 对比 → 一致则通过,否则拒绝

你想想看,这三段的关系其实很像快递包裹:Header 是快递单上的服务类型(空运/陆运),Payload 是包裹里的东西,Signature 是封条。封条坏了,东西可能被换过,不能收。

3.5 常见误区与最佳实践

我在项目中见过不少 JWT 使用上的坑,这里列几个最常见的:

误区 后果 正确做法
Payload 放敏感信息 信息泄露 只放非敏感标识,敏感数据走 API
不校验 exp 令牌永久有效 每次请求都校验 exp
不校验 alg alg=none 攻击 白名单化允许的算法
签名密钥硬编码 密钥泄露后无法快速轮换 使用密钥管理服务,支持动态轮换
Payload 过大 HTTP Header 超限 控制 Payload 在 1KB 以内

我的习惯:每次签发 JWT 时,我都会检查 Payload 里有没有不该出现的数据。另外,我会在 Header 里加一个 "kid" 字段,方便密钥轮换时快速定位用哪个密钥验证。这个小细节,在微服务架构里特别有用。

最后说一句:JWT 的结构设计其实很优雅。它把算法、数据、校验分得清清楚楚。但再好的设计,用错了地方也是白搭。理解这三段各自的责任边界,你就能避开 90% 的常见坑。