4. JWT Payload 详解:注册声明、公共声明与私有声明
JWT 的 Payload(载荷)是令牌中承载实际数据的部分,其结构是一个 Base64Url 编码的 JSON 对象。根据 RFC 7519 规范,Payload 中的声明(Claim)被划分为三类:注册声明(Registered Claims)、公共声明(Public Claims) 和 私有声明(Private Claims)。理解这三类声明的边界与用途,是构建安全、可互操作 JWT 系统的基础。
核心原则: Payload 仅做声明(Claim)的载体,不应对敏感数据(如密码、完整信用卡号)进行明文存储。即使 JWT 使用签名防止篡改,Payload 本身仅做 Base64Url 编码,并非加密。敏感信息必须通过 JWE(JSON Web Encryption)或额外加密层保护。
4.1 注册声明(Registered Claims)
注册声明是 RFC 7519 预定义的一组标准化声明名称,用于提供通用的、可互操作的语义。它们并非强制使用,但强烈建议在适用场景下采用,以提升跨系统的兼容性。以下是七个核心注册声明:
| 声明名 | 全称 | 类型 | 语义与约束 | 典型值示例 |
|---|---|---|---|---|
iss |
Issuer | 字符串 / URI | 签发该 JWT 的主体。验证方应校验 iss 是否与信任的签发方一致。 |
"https://auth.example.com" |
sub |
Subject | 字符串 | JWT 所描述的主体(通常是用户 ID)。在同一个 iss 范围内应全局唯一。 |
"user_123456" |
aud |
Audience | 字符串 / 字符串数组 | JWT 的接收方。验证方必须确认自身在 aud 列表中,否则应拒绝。 |
"api-gateway" 或 ["service-a", "service-b"] |
exp |
Expiration Time | NumericDate | 过期时间戳(秒)。必须在 exp 之后拒绝该令牌。 |
1735689600 (2025-01-01 00:00:00 UTC) |
nbf |
Not Before | NumericDate | 生效时间戳(秒)。在该时间之前,令牌不应被接受。 | 1735603200 |
iat |
Issued At | NumericDate | 签发时间戳(秒)。用于判断令牌的“年龄”或进行日志审计。 | 1735600000 |
jti |
JWT ID | 字符串 | 令牌的唯一标识符。用于防止重放攻击(Replay Attack),服务端需维护已使用的 jti 黑名单。 |
"550e8400-e29b-41d4-a716-446655440000" |
时间声明安全要点:
- 时钟偏差(Clock Skew): 验证服务应允许最多 30-60 秒的时钟偏差,避免因服务器时间不同步导致合法令牌被误拒。
exp与nbf组合: 建议同时使用exp和nbf,形成严格的时间窗口。例如:nbf: 1735603200, exp: 1735689600。jti生命周期: 服务端只需在exp之前维护jti黑名单,过期后可自动清理,避免存储无限增长。
4.2 公共声明(Public Claims)
公共声明是由 IANA JSON Web Token Claims Registry 注册的声明名称,或使用 Collision-Resistant Namespace(抗冲突命名空间)定义的声明。它们用于在开放生态中共享标准化的业务语义,避免不同系统间的命名冲突。
- IANA 注册声明: 例如
name、email、picture、email_verified等。这些声明由 IANA 维护,任何系统都可以引用,具有明确的语义。 - 抗冲突命名空间: 使用 URI 或域名作为声明名称的前缀。例如:
"https://example.com/claims/role"或"com.example.tenant_id"。这种方式确保声明名称在全球范围内唯一。
最佳实践: 如果你的系统需要定义新的、可能被外部系统使用的声明,优先考虑注册到 IANA 或使用 URI 命名空间。避免使用简单字符串(如
"role"),因为可能与未来其他系统的公共声明冲突。
4.3 私有声明(Private Claims)
私有声明是 JWT 签发方和验证方之间私下约定的声明名称。它们不经过 IANA 注册,也不使用命名空间。私有声明适用于内部系统、微服务间通信或特定业务场景。
- 使用场景: 内部用户角色、权限标识、会话 ID、租户 ID、请求追踪 ID 等。
- 风险: 命名冲突。如果两个不同的内部系统使用了相同的私有声明名称但语义不同,可能导致安全漏洞或逻辑错误。
- 缓解措施: 在团队或组织内部建立声明命名规范,例如统一前缀(如
app_、svc_)。
// 示例:包含三类声明的 JWT Payload
{
// 注册声明
"iss": "https://auth.example.com",
"sub": "user_789012",
"aud": ["api-gateway", "order-service"],
"exp": 1735689600,
"nbf": 1735603200,
"iat": 1735600000,
"jti": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
// 公共声明(IANA 注册)
"name": "张三",
"email": "zhangsan@example.com",
"email_verified": true,
// 私有声明(内部约定)
"app_role": "admin",
"app_tenant_id": "tenant_42",
"app_session_id": "sess_abc123"
}
4.4 三类声明的对比与选择
| 维度 | 注册声明 | 公共声明 | 私有声明 |
|---|---|---|---|
| 标准化程度 | RFC 标准,全球通用 | IANA 注册或 URI 命名空间 | 内部约定,无标准化 |
| 命名冲突风险 | 极低(RFC 保留) | 低(URI 保证唯一性) | 高(需内部规范) |
| 互操作性 | 高(所有 JWT 库支持) | 中(需双方理解语义) | 低(仅限约定双方) |
| 适用场景 | 令牌生命周期、身份标识 | 用户画像、开放 API 标准字段 | 内部业务逻辑、微服务上下文 |
| 推荐度 | 必须使用(时间相关) | 推荐(开放生态) | 谨慎使用(需规范) |
4.5 Payload 安全设计原则
- 最小化原则: Payload 只包含验证和授权所必需的声明。避免携带大量业务数据,以减少令牌体积和泄露风险。
- 敏感数据保护: 密码、密钥、完整身份证号等敏感信息严禁放入 Payload。如需传递,使用 JWE 加密整个 JWT 或对敏感字段单独加密。
- 声明验证: 服务端必须对
iss、aud、exp、nbf等关键声明进行严格校验。忽略这些校验是 JWT 安全漏洞的常见来源。 - 声明命名规范: 私有声明使用统一前缀(如
app_、svc_),公共声明优先使用 IANA 注册名称或 URI 命名空间。 - 时间戳精度: 使用
NumericDate(秒级 Unix 时间戳),避免使用字符串格式的时间,以减少解析歧义。
实战建议: 在微服务架构中,建议将
aud 声明设置为目标服务的标识列表。例如,一个令牌同时用于访问订单服务和支付服务时,aud 可设为 ["order-service", "payment-service"]。每个服务在验证时只接受包含自身标识的令牌,实现细粒度的服务间授权。