JWT 结构详解:Header、Payload、Signature 三部分

JWT 说白了就是个三段式的字符串,用点号隔开。每段都有自己的使命。我在早期做微服务网关时,经常看到新手直接把用户信息塞进 Payload 还不加密,以为 JWT 就是加密的——其实它只是 Base64 编码,谁都能解码出来。嗯,我们先拆开看看。

Header(头部):告诉别人我怎么签的

Header 是一个 JSON 对象,指定了令牌的类型和签名算法。我习惯把它叫做“算法的声明区”。

标准字段

  • typ:令牌类型,固定为 "JWT"
  • alg:签名算法,常用 HS256(对称)、RS256(非对称)
  • kid:密钥 ID(非必须,多密钥场景常用)

举个例子,Header 解码后的样子:

{
  "alg": "HS256",
  "typ": "JWT"
}

注意这里 alg 的值如果被改成 none,攻击者就可能绕开签名验证。我曾经审计过一个老项目,发现它居然允许 alg: none——这意味着任何人都可以随意伪造令牌。那天下午我加了一个白名单校验,只允许 HS256 和 RS256。

坑: 永远不要在生产环境信任客户端传来的 alg 值。服务端必须预设可用的算法列表,否则容易被“算法混淆攻击”。

Payload(载荷):数据主体,但别放敏感信息

Payload 是 JWT 的核心数据区,存放声明(Claims)。这些声明分为三类:

类型 字段 说明
注册声明 iss(签发者)、sub(主题)、aud(受众)、exp(过期时间)、nbf(生效时间)、iat(签发时间)、jti(唯一编号) 标准化字段,建议按规范使用
公共声明 自定义字段,例如 user_id、role 防止冲突最好用 URI 命名
私有声明 和特定服务约定的字段 仅内部使用

看一个实际 Payload:

{
  "sub": "1234567890",
  "name": "李晓明",
  "iat": 1716200000,
  "exp": 1716286400,
  "role": "admin"
}

这里有个关键点:Payload 只是 Base64 编码,不是加密。Base64 解码后任何人都能读取。所以我在设计接口时有一条铁律:永远不要把密码、手机号、身份证号等敏感数据直接放在 Payload 里。如果需要传输这些,要么用 JWE(加密的 JSON Web ),要么在 Payload 里只放一个不可逆的标识 ID。

我习惯把 exp 和 iat 当作必填字段。每次签发令牌时都检查系统时间,避免时钟偏差导致误判。曾经有一个项目因为两台服务器的系统时间差了 30 秒,导致令牌一签发就过期——排查了两个小时才发现。

Signature(签名):防篡改的锁

Signature 是对 Header 和 Payload 的完整性保护。它的生成公式是:

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

或者使用非对称算法 RS256,用私钥签名,公钥验证。

过程很清晰:把前两段用 Base64URL 编码(注意不是标准 Base64,要替换 + / 为 - _,去掉 =),拼接后加上密钥或私钥,然后做哈希。接收方拿到令牌后,用自己的密钥或公钥重新算一遍签名,如果一样说明数据没被改过。

我记得有一次线上问题:客户端用 HS256 签名,服务端却用 RS256 的密钥去验证——结果所有令牌都显示“签名无效”。后来我强制在 Header 里加了 kid 字段,并在服务端缓存了密钥映射表,这样切换算法时才不会混乱。

整体组装:三段式

一个完整的 JWT 长这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Iuadj-a0geaYjiIsImlhdCI6MTcxNjIwMDAwMCwiZXhwIjoxNzE2Mjg2NDAwLCJyb2xlIjoiYWRtaW4ifQ.
7sJ0a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0

三段之间用点号连接。你可以用 jwt.io 在线调试器看到每段解码后的内容。但记住:调试器会把你的密钥和令牌发到服务器上,生产环境千万别用。我一般在本地的 Python 脚本里用 pyjwt 库手动调试。

总结三部分的作用

  • Header:告诉服务端“我用的什么算法”,是协商元数据
  • Payload:存储实际数据声明,但可读不安全
  • Signature:用密钥或私钥生成的数字指纹,保证数据未被篡改

SVG 展示:JWT 结构流程

下面这张图把 JWT 的生成与验证流程浓缩在一起,方便你理解三部分的关系。

JWT 结构:生成与验证流程 Header {"alg":"HS256", "typ":"JWT"} Payload {"sub":"123", "exp":1716286400} 密钥/私钥 "my-secret" Base64URL 编码 header(ba64) + "." + payload(b64) 拼接成字符串 → 例如:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0 签名算法 (HS256 / RS256) HMACSHA256( 拼接字符串, secret ) → 输出 Signature 字符串 完整 JWT 令牌 header.b64 + "." + payload.b64 + "." + signature eyJ... .eyJ... .7sJ0a1b2c3d4e5f6g... 验证过程(接收端) 1. 分割JWT,取前两段 2. 用相同密钥/公钥重新计算签名 3. 比对收到的签名是否一致 4. 检查exp/nbf等时间声明 Header 声明算法,Payload 存放数据,Signature 保证完整性 —— 三者缺一不可

调试小技巧: 在本地用 Python 的 jwt 库 jwt.encode() 时,可以打印出三个部分的 Base64 字符串,再用 base64.urlsafe_b64decode() 手动解码,加深理解。

重要提醒: 永远不要相信客户端发送的 alg 值。我见过攻击者把 RS256 改成 HS256,然后用获取到的公钥作为 HMAC 密钥来伪造签名(因为公钥通常是公开的)。破解方法很简单:服务端固定一套算法白名单。