3、JWT签名算法:HS256(对称签名)与RS256(非对称签名)的原理与选择场景
JWT 的签名算法决定了令牌的完整性与可信来源。生产环境中最常见的两种算法是 HS256(HMAC with SHA-256,对称签名)与 RS256(RSA with SHA-256,非对称签名)。理解它们的数学基础、密钥管理差异以及适用场景,是构建安全认证契约的第一步。
3.1 核心原理对比
| 对比维度 | HS256(对称) | RS256(非对称) |
|---|---|---|
| 密钥类型 | 单一共享密钥(Secret) | 私钥(Private Key)+ 公钥(Public Key) |
| 签名与验签 | 签名与验签使用同一个密钥 | 私钥签名,公钥验签 |
| 算法族 | HMAC-SHA256 | RSA-SHA256 |
| 计算开销 | 低(对称哈希运算) | 高(大数模幂运算) |
| 密钥分发 | 需安全通道共享给所有验证方 | 公钥可公开分发,私钥仅签名方持有 |
| 典型 Token 长度 | 较短(约 200-300 字符) | 较长(约 500-800 字符) |
3.2 对称签名:HS256 工作原理
HS256 使用 HMAC(基于哈希的消息认证码)结合 SHA-256 哈希函数。签名方与验证方必须持有完全相同的 共享密钥。签名过程本质上是将 JWT 的 Header 和 Payload 与密钥混合后进行哈希运算,生成一个固定长度的摘要作为签名。
关键约束:共享密钥一旦泄露,攻击者可以伪造任意 JWT。因此 HS256 适用于 单一服务内部 或 完全可信的微服务集群 之间,且密钥必须通过环境变量或密钥管理服务(KMS)注入,禁止硬编码在代码仓库中。
3.3 非对称签名:RS256 工作原理
RS256 基于 RSA 公钥密码体系。签发服务使用 私钥 对 JWT 签名,任何持有对应 公钥 的第三方服务都可以验证签名的真实性,但无法伪造新的令牌。这种“私钥签名、公钥验签”的分离特性,使得 RS256 天然适合 开放生态 与 多客户端验证 场景。
推荐实践:在微服务架构中,建议由统一的认证中心(Auth Service)持有私钥签发 JWT,其他业务服务只持有公钥进行验签。这样即使某个业务服务被攻破,攻击者也无法获取私钥来伪造令牌。
3.4 算法选择决策流程
以下 SVG 图展示了从系统架构角度选择 HS256 或 RS256 的决策路径:
3.5 安全风险与避坑指南
- 算法混淆攻击:攻击者将 JWT 的
alg头改为none或HS256(当服务端预期 RS256 时)。务必在验签逻辑中 白名单校验算法类型,拒绝非预期算法。 - 密钥轮换:HS256 的共享密钥应定期轮换(建议 90 天),且轮换期间需保留旧密钥窗口以验证未过期的 Token。RS256 可通过
kid(Key ID)字段支持多公钥轮换。 - 密钥长度:HS256 密钥至少 256 位(32 字节);RS256 私钥推荐 2048 位以上,低于 1024 位已被认为不安全。
生产事故案例:某团队在微服务网关中使用 HS256,将共享密钥硬编码在配置文件中并上传至 Git 仓库。内部员工泄露仓库后,攻击者伪造管理员 JWT 获取了全部 API 权限。事后整改为 RS256 + 私钥存储在硬件安全模块(HSM)中。
3.6 代码示例:HS256 与 RS256 签名与验签
以下示例使用 Node.js 的 jsonwebtoken 库展示两种算法的核心差异:
// ========== HS256 对称签名 ==========
const jwt = require('jsonwebtoken');
const SECRET = 'your-256-bit-secret-min-32-chars!!';
// 签名
const hsToken = jwt.sign({ userId: 1001, role: 'admin' }, SECRET, { algorithm: 'HS256', expiresIn: '1h' });
// 验签(同一密钥)
const decoded = jwt.verify(hsToken, SECRET, { algorithms: ['HS256'] });
console.log('HS256 验证通过:', decoded);
// ========== RS256 非对称签名 ==========
const fs = require('fs');
const privateKey = fs.readFileSync('./private.pem', 'utf8');
const publicKey = fs.readFileSync('./public.pem', 'utf8');
// 签名(私钥)
const rsToken = jwt.sign({ userId: 1001, role: 'admin' }, privateKey, { algorithm: 'RS256', expiresIn: '1h' });
// 验签(公钥)
const decodedRS = jwt.verify(rsToken, publicKey, { algorithms: ['RS256'] });
console.log('RS256 验证通过:', decodedRS);
3.7 选择场景总结
| 场景 | 推荐算法 | 理由 |
|---|---|---|
| 单体应用内部 Session 替换 | HS256 | 单一验证方,性能优先,密钥管理简单 |
| 微服务架构(认证中心签发) | RS256 | 多服务验签,私钥不泄露,支持公钥公开 |
| 第三方开放 API / SSO | RS256 | 客户端可获取公钥自行验证,无需共享密钥 |
| IoT 设备 / 低算力环境 | HS256 | 计算开销低,Token 体积小,适合带宽受限场景 |
| 合规要求严格的金融系统 | RS256 | 非对称体系满足审计要求,私钥可硬件隔离 |
核心原则:能用 RS256 就不用 HS256。非对称签名的安全边界远优于对称签名,尤其在服务数量增长、团队边界扩大的情况下,RS256 的密钥管理成本远低于 HS256 泄露后的灾难恢复成本。