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 的生成与验证流程浓缩在一起,方便你理解三部分的关系。
调试小技巧: 在本地用 Python 的 jwt 库 jwt.encode() 时,可以打印出三个部分的 Base64 字符串,再用 base64.urlsafe_b64decode() 手动解码,加深理解。
重要提醒: 永远不要相信客户端发送的 alg 值。我见过攻击者把 RS256 改成 HS256,然后用获取到的公钥作为 HMAC 密钥来伪造签名(因为公钥通常是公开的)。破解方法很简单:服务端固定一套算法白名单。