JWT(JSON Web Token)结构详解:Header、Payload、Signature与编解码
004、JWT结构详解:Header、Payload、Signature与编解码
昨天排查线上问题,一个微服务间的接口突然返回403。日志里只有一句“Invalid token”,抓包看到Authorization头里明明带着Token,格式也没错。最后发现是某个服务偷偷升级了JWT库,签名算法默认配置变了。这种问题不深入理解JWT的结构,根本无从下手。
今天我们就拆开一个JWT,看看它到底是怎么组装起来的。
一、先看一个完整的JWT长什么样
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
注意中间的两个点,把字符串分成了三段。这就是JWT的标准格式:Header.Payload.Signature。每段都是Base64Url编码后的JSON,别被那一长串字符吓到,解码看看就明白了。
二、Header:元数据声明
第一段解码后是这样的:
{
"alg": "HS256",
"typ": "JWT"
}
alg指定签名算法,常见的有HS256(HMAC SHA-256)、RS256(RSA SHA-256)。这里有个坑:如果你用非对称加密(比如RS256),别在客户端存私钥——私钥只能放在签发服务端。
typ通常就是"JWT",有些老库会检查这个字段。我遇到过把typ写成"JWT "(多一个空格)导致验证失败的案例。
三、Payload:实际携带的数据
第二段解码后:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
这些字段叫Claim(声明)。分三类:
注册声明(建议但不强制使用):
- iss(签发者)
- exp(过期时间戳,这个一定要设!)
- sub(主题)
- aud(接收方)
公共声明:可以自定义,但最好避免和已注册声明冲突。我习惯加个"uid"放用户ID,"role"放角色。
私有声明:业务自定义字段。注意别塞敏感数据(比如密码),因为Payload只是Base64编码,不是加密。见过有人把手机号明文放进去,被中间人抓包直接解码出来。
时间戳字段(iat、exp、nbf)都是Unix时间戳,单位是秒。这里容易踩坑:有些语言库默认用毫秒,生成和验证时单位要对齐。
四、Signature:防篡改的关键
签名是这样生成的(伪代码):
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
注意拼接的是编码后的字符串,不是解码后的JSON对象。验证时服务端用同样的算法重新计算一次,和传过来的第三段对比。不一致就说明Token被改过。
如果是RS256非对称加密,要用私钥签名、公钥验证。部署时千万别把公钥私钥搞反——线上出现过用公钥签名导致所有验证都失败的故障。
五、编解码实战细节
Base64Url和标准Base64的区别:把+换成-,/换成_,去掉末尾的=。有些语言的Base64库需要手动处理这个转换。
解码时先分段:
# 实际代码要处理异常,这里简写
header_b64, payload_b64, signature_b64 = token.split('.')
然后分别Base64Url解码。注意Payload解码后是JSON字符串,要解析成对象才能用。
自己写编解码练习一下:
import base64
import json
# 模拟生成
header = {"alg": "HS256", "typ": "JWT"}
payload = {"user_id": 123, "exp": 1741000000}
header_b64 = base64.urlsafe_b64encode(json.dumps(header).encode()).rstrip(b'=')
payload_b64 = base64.urlsafe_b64encode(json.dumps(payload).encode()).rstrip(b'=')
# 签名部分省略...
调试时可以在jwt.io这个网站粘贴Token直接解码查看(注意别贴生产环境的真实Token)。
六、几个容易栽跟头的地方
-
算法不一致问题:Header里写HS256,验证时用RS256,肯定失败。建议服务端强制校验alg字段,防止算法替换攻击。
-
时间漂移:多台服务器时间不同步,导致exp验证不准。最好留点余量(比如给5分钟容忍)。
-
Base64编码陷阱:有些库的Base64Url实现会补等号,有些不会。跨语言传递时先统一格式。
-
Claim名称冲突:自定义字段别起名叫"iss"、"exp"这些保留字。曾经有同事在Payload里加了个"exp"字段想表示“过期原因”,结果直接导致Token被判定为已过期。
-
签名密钥管理:HS256的密钥要足够长(建议32字节以上),定期轮换。见过用"secret"当密钥的,暴力破解几分钟就出来了。
最后说点经验
看懂了结构,开头那个403问题就好解决了:新库默认用了RS256,而旧Token是HS256签名的。要么回滚库版本,要么做兼容处理——验证时先读Header里的alg字段,再选对应算法。
实际开发中,我建议在Payload里最少放三个字段:用户ID(uid)、过期时间(exp)、签发时间(iat)。角色权限这类经常变的数据,最好别放Token里,而是查数据库或缓存。Token本身应该尽量轻量,毕竟每次请求都要带在头上。
下次遇到JWT验证失败,别急着抓瞎。按这个顺序查:先看签名是否一致(算法/密钥问题),再看时间是否有效(时钟同步问题),最后看Claim是否符合预期(业务逻辑问题)。拆成三段看,问题往往就藏在那几个字节里。
(注:所有代码示例均为说明性伪代码,生产环境请使用成熟的JWT库并处理异常情况。)
更多推荐

所有评论(0)