4. Token 生成与签发:服务端生成 Token 流程、签名算法选择(HS256/RS256)、Payload 设计规范

Token 的生成与签发是鉴权链路的第一道关口。服务端必须在用户登录或凭证校验通过后,按照标准流程构造 JWT(JSON Web Token),选择合适的签名算法,并设计合理的 Payload 结构。本章将拆解这三个核心环节,确保生成的 Token 既安全又具备可扩展性。

4.1 服务端生成 Token 的标准流程

一个完整的 Token 签发流程包含以下 6 个步骤,每一步都直接影响后续的验证效率与安全性:

  1. 用户身份校验:接收用户名/密码、OAuth 授权码或第三方凭证,通过数据库或认证服务验证合法性。
  2. 构造 JWT Header:固定包含 typ: "JWT" 和 alg 字段(如 HS256 或 RS256)。
  3. 构造 JWT Payload:按规范填充标准声明(iss、sub、exp、iat、jti)和自定义声明(用户角色、权限范围、会话 ID)。
  4. 计算签名:使用选定的算法和密钥对 Header + Payload 进行签名,生成 Signature 部分。
  5. 组装 Token:将 Base64URL 编码的 Header、Payload 和 Signature 用点号(.)连接。
  6. 缓存与下发:将 Token 存入 Redis(用于黑名单/刷新逻辑),并通过 HTTP 响应返回给客户端。
关键指标:Token 签发耗时应控制在 5ms 以内(含 Redis 写入),若超过 10ms 需检查签名算法或 Redis 连接池配置。
Token 签发流程(服务端) 1. 用户身份校验 2. 构造 JWT Header 3. 构造 Payload 4. 计算签名 5. 组装 Token 6. 缓存与下发 ⚠ 注意:步骤 6 中 Redis 缓存建议使用 EXPIRE 命令设置 TTL,与 Token 的 exp 声明保持一致。

4.2 签名算法选择:HS256 vs RS256

签名算法决定了 Token 的防篡改能力和密钥管理方式。HS256(HMAC-SHA256)和 RS256(RSA-SHA256)是生产环境中最常用的两种算法,它们的核心差异如下:

对比维度 HS256(对称) RS256(非对称)
密钥类型 单一共享密钥(Secret) 私钥签名 + 公钥验证
签名速度 快(约 0.5ms) 慢(约 2-5ms)
验证速度 快(约 0.5ms) 快(约 0.8ms)
密钥分发 需安全通道共享 Secret 公钥可公开,私钥仅签发端持有
适用场景 单体服务、内部微服务 多服务、第三方集成、开放 API
密钥轮换 需同步更新所有服务 仅需更新签发端私钥
选型建议:如果你的后端架构是单体应用或内部微服务(所有服务都在同一信任域),优先选择 HS256 以获得更快的签名速度。如果涉及第三方服务、开放 API 或需要独立验证 Token(如 API 网关),选择 RS256 更安全,因为公钥可以安全地分发给所有验证方。

4.3 Payload 设计规范

JWT Payload 是 Token 的信息载体,设计时需遵循“最小必要原则”——只包含验证和授权所必需的字段,避免携带敏感数据。Payload 分为标准声明(Registered Claims)和自定义声明(Custom Claims)两类。

4.3.1 标准声明字段

声明 全称 说明 是否必选
iss Issuer 签发者标识,如 "auth-service" 推荐
sub Subject 用户唯一标识(如 user_id) 必选
exp Expiration Time 过期时间(Unix 时间戳),建议 15-60 分钟 必选
iat Issued At 签发时间(Unix 时间戳) 推荐
jti JWT ID Token 唯一标识(UUID),用于防重放 推荐

4.3.2 自定义声明设计示例

以下是一个生产级 Payload 示例,包含标准声明和必要的自定义声明:

{
  "iss": "auth-api.v2.example.com",
  "sub": "user_8a3f7b2e",
  "exp": 1712345678,
  "iat": 1712341778,
  "jti": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "role": "admin",
  "scope": ["read:orders", "write:orders"],
  "session_id": "sess_9f8e7d6c5b4a"
}
安全红线:
• 禁止在 Payload 中存储密码、身份证号、银行卡号等敏感信息(Payload 仅 Base64URL 编码,非加密)。
• 自定义声明键名使用小驼峰或下划线命名,避免与标准声明冲突。
• exp 和 iat 必须使用 UTC 时间戳,避免时区问题。
JWT Payload 结构分解 标准声明(Registered Claims) iss (签发者) sub (用户ID) exp (过期时间) iat (签发时间) jti (唯一ID) 自定义声明(Custom Claims) role (角色) scope (权限范围) session_id (会话ID) tenant_id (租户) ⚠ 自定义声明应避免超过 10 个字段,单个 Payload 总大小建议控制在 1KB 以内,以减少网络传输和 Redis 存储开销。

4.4 代码实现:Token 签发核心逻辑

以下是一个基于 Node.js + jsonwebtoken 库的 Token 签发示例,展示了 HS256 和 RS256 两种算法的实现方式:

const jwt = require('jsonwebtoken');
const crypto = require('crypto');

// ========== HS256 对称签名 ==========
const HS256_SECRET = process.env.JWT_HS256_SECRET || 'your-256-bit-secret';

function signHS256(userId, role, scope) {
  const payload = {
    iss: 'auth-api.v2.example.com',
    sub: userId,
    iat: Math.floor(Date.now() / 1000),
    exp: Math.floor(Date.now() / 1000) + 3600, // 1小时过期
    jti: crypto.randomUUID(),
    role: role,
    scope: scope
  };

  return jwt.sign(payload, HS256_SECRET, { algorithm: 'HS256' });
}

// ========== RS256 非对称签名 ==========
const fs = require('fs');
const PRIVATE_KEY = fs.readFileSync('./keys/private.pem', 'utf8');

function signRS256(userId, role, scope) {
  const payload = {
    iss: 'auth-api.v2.example.com',
    sub: userId,
    iat: Math.floor(Date.now() / 1000),
    exp: Math.floor(Date.now() / 1000) + 3600,
    jti: crypto.randomUUID(),
    role: role,
    scope: scope
  };

  return jwt.sign(payload, PRIVATE_KEY, { algorithm: 'RS256' });
}

// 使用示例
const token = signHS256('user_8a3f7b2e', 'admin', ['read:orders', 'write:orders']);
console.log('Generated Token:', token);
生产建议:
• 密钥(HS256 Secret 或 RS256 私钥)必须通过环境变量或密钥管理服务(如 AWS KMS、HashiCorp Vault)注入,禁止硬编码在代码仓库中。
• 每次签发 Token 后,将 jti 和 exp 写入 Redis(SETEX jti:<value> <ttl> 1),用于后续的黑名单校验和防重放检测。
• 建议在签发时记录审计日志,包含 sub、jti、iat、客户端 IP 和 User-Agent。