当我第一次接触JWT时,最吸引我的不是它的"无状态"特性,而是它那三部分点分结构的精巧设计。这让我想起TCP/IP协议的分层思想------每个部分职责明确,组合起来却威力无穷。
一、拆解一个真实的JWT
让我们先看一个实际的JWT示例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
这个看似随机的字符串,实际上由三个独立的部分组成,用点(.)分隔:
makefile
Header: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Payload: eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
Signature: SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
二、三部曲的各自使命
1. Header(头部):元数据协议层
json
// Base64解码后的实际内容
{
"alg": "HS256", // 算法声明:使用HMAC SHA-256
"typ": "JWT" // 类型声明:这是一个JWT
}
设计精妙之处:
- 自描述性:Token自己声明了如何验证自己
- 算法协商:服务端可以根据alg字段选择对应的验证方法
- 可扩展性:未来可以添加其他元数据(如kid密钥ID)
后端视角 :这就像HTTP请求头中的Content-Type,告诉接收方"如何解析我"。
2. Payload(载荷):业务数据层
json
// Base64解码后的实际内容
{
"sub": "1234567890", // 标准声明:主题(用户ID)
"name": "John Doe", // 自定义声明:用户名
"iat": 1516239022 // 标准声明:签发时间
}
三类声明清晰划分:
| 声明类型 | 示例 | 作用 | 是否必需 |
|---|---|---|---|
| 注册声明 | iss, exp, sub |
预定义标准字段 | 可选但推荐 |
| 公共声明 | 可自定义 | 公共约定字段 | 可选 |
| 私有声明 | name, role |
业务自定义字段 | 按需添加 |
Payload的设计体现了"约定优于配置"的思想。sub、iat、exp这些标准字段,让不同系统能相互理解Token的基本信息。
3. Signature(签名):安全校验层
scss
Signature = HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
签名的核心价值:
javascript
// 验证伪代码 - 这就是签名的本质
function verifySignature(token, secret) {
const [headerB64, payloadB64, signature] = token.split('.');
// 重新计算签名
const data = headerB64 + "." + payloadB64;
const expectedSignature = hmacSha256(data, secret);
// 对比:任何修改都会被检测到
return signature === expectedSignature;
}
三、设计的精妙之处
1. 分层解耦的智慧
css
客户端视角: 服务端视角:
完整Token ────────▶ 拆解验证
│
├─ Header → 选验证算法
├─ Payload → 取业务数据
└─ Signature → 验完整性
每个部分可以独立处理,正如TCP/IP中物理层、网络层、应用层各司其职。
2. 点分隔符的简洁美学
一个简单的点(.)解决了三个问题:
- 明确边界:无需复杂的分隔协议
- 易于解析 :
token.split('.')即可 - 人类可读:肉眼能看出是三部分
对比其他方案:
- XML:冗长的标签
<header>...</header> - JSON嵌套:需要解析后再拆分
- 自定义分隔符:不标准且容易冲突
四、从结构看JWT的本质
通过分析三部分结构,得到JWT的核心本质:
1. 不是加密协议,是验证协议
JWT的Payload是Base64编码,不是加密!任何人都可以解码看到内容:
bash
# 任何开发者都可以查看
echo "eyJzdWIiOiIxMjM0In0=" | base64 --decode
# 输出: {"sub":"1234"}
签名只保证数据不被篡改,不保证数据机密性。敏感信息绝不应该放在Payload中。
2. 自包含的双刃剑
javascript
// 优点:服务端无需查库即可获得用户信息
const payload = decodeToken(token);
const userId = payload.sub; // 直接获取,无DB查询
// 缺点:信息更新延迟
// 用户权限变更,但旧Token依然有效直到过期
3. 可验证的声明
每个声明都可以被验证:
exp:是否过期?iss:签发者是否正确?aud:目标接收者是否匹配?
这比Session ID强大得多------Session ID只是一个随机字符串,而JWT Token是带有可验证声明的数据结构。
五、实战中的结构思考
1. Header的扩展实践
json
{
"alg": "RS256", // 非对称加密更安全
"typ": "JWT",
"kid": "key-2024-05" // 密钥ID,支持轮换
}
添加kid(Key ID)支持密钥轮换,是生产环境的重要实践。
2. Payload的设计原则
json
{
"sub": "user_123",
"name": "张三",
"roles": ["editor", "viewer"], // 数组存储多个角色
"perms": ["post:write", "user:read"], // 具体权限
"iat": 1715000000,
"exp": 1715003600, // 1小时后过期
"jti": "a1b2c3d4" // 唯一ID,防重放
}
我的经验:
sub用有意义的ID,不要用数据库自增ID(安全考虑)- 角色和权限分开存储,便于细粒度控制
jti(JWT ID)防止重放攻击,适用于:支付、重要状态变更、防重复提交
3. 签名算法的选择
javascript
// 对称加密(简单,但密钥管理难)
HS256: 单密钥,签发和验证用同一个
// 非对称加密(复杂,但更安全)
RS256: 私钥签发,公钥验证
// 私钥安全存储,公钥可以分发
六、总结:简单背后的不简单
JWT的三部分结构初看简单,但深究起来处处体现设计智慧:
- Header:协议层,解决"如何验证"的问题
- Payload:数据层,解决"携带什么"的问题
- Signature:安全层,解决"是否可信"的问题
然而,正如所有精妙设计一样,JWT的结构也带来了相应的挑战------Payload的透明性要求我们谨慎选择存储内容,签名的验证机制要求我们妥善管理密钥。这些正是后续章节要深入探讨的。