4、JWT Payload详解:注册声明、公共声明、私有声明
JWT 的 Payload(载荷)是令牌中承载实际数据的部分,它是一个 JSON 对象,包含一组声明(Claims)。声明是关于实体(通常是用户)和附加数据的陈述。根据 JWT 标准(RFC 7519),Payload 中的声明分为三类:注册声明、公共声明和私有声明。理解这三类声明的边界与用途,是设计安全、可互操作的认证授权系统的关键。
4.1 注册声明(Registered Claims)
注册声明是一组预定义的、标准化的声明,旨在提供互操作性。它们不是强制性的,但强烈建议使用,以帮助不同系统之间理解 JWT 的含义。这些声明由 IANA JSON Web Token Claims 注册表维护。
| 声明名 | 全称 | 类型 | 描述 | 使用建议 |
|---|---|---|---|---|
iss |
Issuer | 字符串或 URI | 签发该 JWT 的实体标识符。通常是发行方的唯一标识,如 https://auth.example.com。 |
必须验证,防止令牌被伪造或跨域使用。 |
sub |
Subject | 字符串或 URI | JWT 所描述的主体,通常是用户的唯一标识(如用户 ID)。 | 必须验证,确保令牌指向正确的用户。 |
aud |
Audience | 字符串或 URI 数组 | JWT 的接收方标识。标识此令牌预期用于哪个或哪些服务。 | 必须验证,防止令牌被重放到其他服务。 |
exp |
Expiration Time | 数字(NumericDate) | JWT 的过期时间。在此时间之后,JWT 被视为无效。 | 必须验证,是防止令牌长期泄露的核心机制。 |
nbf |
Not Before | 数字(NumericDate) | JWT 的生效时间。在此时间之前,JWT 不应被接受。 | 可选验证,用于处理时钟偏差或延迟签发场景。 |
iat |
Issued At | 数字(NumericDate) | JWT 的签发时间。可用于确定令牌的年龄。 | 建议验证,用于计算令牌剩余有效期或触发刷新。 |
jti |
JWT ID | 字符串 | JWT 的唯一标识符。用于防止重放攻击(Replay Attack)。 | 建议验证,服务端需维护已使用 jti 的集合。 |
exp 和 nbf 时,必须考虑客户端与服务端之间的时钟偏差(Clock Skew)。通常允许一个小的容差窗口(如 30 秒),但不应过大,否则会削弱安全性。推荐使用 NTP 同步服务器时间。
4.2 公共声明(Public Claims)
公共声明是由使用 JWT 的各方(如组织、公司、开源项目)自定义的声明。为了避免命名冲突,公共声明应该使用 Collision-Resistant Name(防冲突命名)。推荐的做法是使用 URI 作为声明名,例如 https://example.com/claims/role。这样,即使不同组织使用了相同的声明名,由于 URI 的唯一性,也不会发生冲突。
公共声明通常用于传递应用级别的、非敏感的用户属性或上下文信息。
- 用户角色:
https://api.example.com/claims/role→"admin" - 用户权限:
https://api.example.com/claims/permissions→["read:users", "write:users"] - 用户所属部门:
https://org.example.com/claims/department→"engineering" - 用户时区:
https://app.example.com/claims/timezone→"Asia/Shanghai"
4.3 私有声明(Private Claims)
私有声明是 JWT 的签发方和接收方之间私下约定的声明。它们没有命名空间,因此双方必须事先就声明名称和含义达成一致。私有声明通常用于在信任域内部传递非标准化的、临时的或特定于业务逻辑的数据。
由于没有防冲突机制,私有声明应谨慎使用,尤其是在跨组织或跨系统交互时。如果声明可能被多个系统使用,应将其升级为公共声明。
- 会话 ID:
"session_id": "abc123" - 登录来源:
"login_source": "oauth2" - 临时标志:
"is_new_user": true - 内部版本号:
"version": 2
4.4 三类声明的关系与选择
下图展示了三类声明在 JWT Payload 中的共存关系,以及它们各自的应用边界。
4.5 声明验证流程
服务端在接收到 JWT 后,必须对 Payload 中的关键声明进行验证,以确保令牌的合法性和有效性。以下是一个典型的验证流程:
- 验证签名:使用公钥或共享密钥验证 JWT 的签名是否与 Header 和 Payload 匹配。这是所有验证的前提。
- 验证
iss:检查签发者是否在受信任的签发者列表中。 - 验证
aud:检查当前服务是否在令牌的接收方列表中。 - 验证
exp:检查当前时间是否早于过期时间(考虑时钟偏差)。 - 验证
nbf:如果存在,检查当前时间是否晚于生效时间(考虑时钟偏差)。 - 验证
jti:如果存在,检查该 ID 是否已被使用(防止重放)。 - 验证
sub:确保主体标识有效且存在于系统中。
exp、nbf),再验证业务声明(iss、aud、sub、jti)。这样可以尽早拒绝无效令牌,减少不必要的计算开销。
4.6 声明设计对比
| 特性 | 注册声明 | 公共声明 | 私有声明 |
|---|---|---|---|
| 标准化程度 | 高(RFC 标准) | 中(建议 URI 命名) | 低(双方约定) |
| 命名冲突风险 | 无(预定义) | 低(URI 保证唯一性) | 高(无命名空间) |
| 互操作性 | 强(所有 JWT 库支持) | 中(需双方解析 URI) | 弱(仅限内部系统) |
| 典型用途 | 安全控制、令牌生命周期 | 应用级用户属性 | 临时或内部业务数据 |
| 推荐场景 | 所有 JWT 都应包含 | 跨服务共享用户信息 | 微服务内部通信 |
4.7 完整 Payload 示例
以下是一个包含三类声明的 JWT Payload 示例:
{
// 注册声明
"iss": "https://auth.example.com",
"sub": "user_1234567890",
"aud": ["https://api.example.com", "https://admin.example.com"],
"exp": 1718000000,
"nbf": 1717996400,
"iat": 1717996400,
"jti": "unique-token-id-abc-123",
// 公共声明
"https://api.example.com/claims/role": "editor",
"https://org.example.com/claims/department": "engineering",
// 私有声明
"session_id": "sess_xyz_789",
"login_source": "sso"
}