3. JWT 签名算法选型:HMAC、RSA、ECDSA 与强度对比
JWT 的核心安全边界由签名算法决定。选错算法或配置不当,会导致令牌伪造、密钥泄露或性能瓶颈。本节从算法原理、适用场景、安全强度三个维度,对比 HMAC、RSA、ECDSA 三大类签名算法,并给出选型决策依据。
3.1 算法分类与核心差异
JWT 标准(RFC 7519)定义的签名算法分为两类:对称签名(HMAC)和非对称签名(RSA / ECDSA)。对称签名使用同一个密钥进行签名和验签;非对称签名使用私钥签名、公钥验签。下表列出主流算法的关键参数:
| 算法族 | JWT 头 alg 值 | 密钥类型 | 签名长度 | 计算速度 | 典型场景 |
|---|---|---|---|---|---|
| HMAC-SHA256 | HS256 | 对称(共享密钥) | 32 字节 | 极快 | 单体服务、内部微服务间信任 |
| HMAC-SHA384 | HS384 | 对称(共享密钥) | 48 字节 | 快 | 对安全要求更高的内部系统 |
| HMAC-SHA512 | HS512 | 对称(共享密钥) | 64 字节 | 中等 | 极高安全内部链路 |
| RSA-SHA256 | RS256 | 非对称(RSA 2048+) | 256 字节 | 慢(签名) | 跨服务、跨组织鉴权 |
| RSA-SHA384 | RS384 | 非对称(RSA 2048+) | 384 字节 | 慢 | 金融、政务等高安全场景 |
| RSA-SHA512 | RS512 | 非对称(RSA 2048+) | 512 字节 | 慢 | 合规要求极高的场景 |
| ECDSA-P256 (SHA256) | ES256 | 非对称(ECC P-256) | 64 字节 | 快 | 移动端、IoT、高并发微服务 |
| ECDSA-P384 (SHA384) | ES384 | 非对称(ECC P-384) | 96 字节 | 中等 | 高安全、低带宽场景 |
| ECDSA-P521 (SHA512) | ES512 | 非对称(ECC P-521) | 132 字节 | 中等 | 顶级安全合规场景 |
选型第一原则:只要涉及跨信任域(不同团队、不同组织、公网环境),必须使用非对称签名(RS256 / ES256)。对称签名 HMAC 仅限同一信任域内的内部通信。
3.2 HMAC 对称签名:简单但边界脆弱
HMAC(Hash-based Message Authentication Code)结合哈希函数与共享密钥,计算速度快、实现简单。但对称密钥必须通过安全通道分发,一旦泄露,攻击者可伪造任意令牌。
- 优点:计算开销极低,适合高吞吐内部调用;密钥长度灵活(推荐 ≥ 256 位)。
- 缺点:密钥分发困难;无法实现非对称的“私钥签名、公钥验签”模型;验签方也能签名,无法做抗抵赖。
- 典型陷阱:将 HMAC 密钥硬编码在代码仓库中;使用弱密钥(如短于 256 位)。
// 正确生成 HMAC 密钥(Node.js crypto 示例)
const crypto = require('crypto');
const secret = crypto.randomBytes(32).toString('hex');
// 输出:a3f2b8c1...(64 个十六进制字符,256 位)
console.log(secret);
3.3 RSA 非对称签名:成熟但沉重
RSA 是目前最广泛使用的非对称签名算法。私钥签名、公钥验签的模型天然适合分布式系统:认证服务持有私钥,下游服务只需公钥即可验签,无需共享敏感密钥。
- 优点:公钥可公开分发;支持数字证书(X.509)体系;生态成熟,几乎所有语言都有标准库支持。
- 缺点:签名体积大(RS256 签名 256 字节);签名速度慢(尤其私钥操作);密钥对生成耗时。
- 推荐密钥长度:至少 2048 位;生产环境建议 4096 位。
安全警告:禁止使用 RS256 配合 1024 位以下 RSA 密钥。2024 年后,1024 位 RSA 已被证明可在合理成本内破解。务必使用 2048 位或更高。
3.4 ECDSA 椭圆曲线签名:现代高性能选择
ECDSA(Elliptic Curve Digital Signature Algorithm)基于椭圆曲线密码学,在相同安全强度下,密钥更短、签名更小、计算更快。P-256(secp256r1)曲线提供与 RSA 2048 等价的安全强度,但签名长度仅 64 字节。
- 优点:签名体积小(ES256 仅 64 字节);签名和验签速度均优于 RSA;带宽和存储开销低。
- 缺点:曲线选择复杂(P-256 / P-384 / P-521 / Curve25519 等);部分老旧库支持不完整;随机数生成质量要求高。
- 推荐曲线:通用场景使用 P-256(ES256);高安全场景使用 P-384(ES384)。
// 生成 ES256 密钥对(Node.js crypto 示例)
const { generateKeyPairSync } = require('crypto');
const { publicKey, privateKey } = generateKeyPairSync('ec', {
namedCurve: 'P-256', // 等价于 secp256r1
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});
console.log(privateKey); // 私钥 PEM 格式
console.log(publicKey); // 公钥 PEM 格式
3.5 算法强度对比表
下表从安全强度、性能、体积三个维度,对主流算法进行横向对比。安全强度以 NIST 推荐的“比特安全强度”为基准。
| 算法 | 安全强度(比特) | 等效 RSA 密钥长度 | 签名体积(字节) | 签名速度(相对) | 验签速度(相对) |
|---|---|---|---|---|---|
| HS256 | 128 | RSA 3072 | 32 | 极快 | 极快 |
| HS384 | 192 | RSA 7680 | 48 | 快 | 快 |
| HS512 | 256 | RSA 15360 | 64 | 中等 | 中等 |
| RS256 (2048) | 112 | RSA 2048 | 256 | 慢 | 中等 |
| RS256 (4096) | 128 | RSA 4096 | 512 | 很慢 | 慢 |
| ES256 (P-256) | 128 | RSA 3072 | 64 | 快 | 快 |
| ES384 (P-384) | 192 | RSA 7680 | 96 | 中等 | 中等 |
| ES512 (P-521) | 256 | RSA 15360 | 132 | 中等 | 中等 |
关键结论:ES256(P-256)在安全强度 128 比特下,签名体积仅为 RS256 的 1/4,签名速度快 5-10 倍。对于新系统,优先选择 ES256 而非 RS256。
3.6 签名算法选型决策流程
下图展示从业务需求出发的签名算法选型决策树:
3.7 常见选型陷阱与规避
- 陷阱一:在跨域场景使用 HMAC。后果:密钥泄露后所有下游服务均可伪造令牌。规避:跨域必须使用非对称签名。
- 陷阱二:使用 RS256 但密钥长度不足 2048 位。后果:安全强度低于 112 比特,存在被破解风险。规避:强制要求 RSA 密钥 ≥ 2048 位。
- 陷阱三:ECDSA 随机数重用。后果:私钥可被直接计算出来。规避:使用安全的随机数生成器,或使用 EdDSA(Ed25519)等确定性签名算法。
- 陷阱四:算法混淆攻击(alg:none)。攻击者将 JWT 头中的 alg 改为 "none" 绕过签名验证。规避:服务端必须白名单校验 alg 字段,拒绝 "none" 和未知算法。
生产强制规则:JWT 验签时,必须显式指定允许的算法列表,禁止从 JWT 头部动态读取 alg 并直接使用。示例:
jwt.verify(token, publicKey, { algorithms: ['ES256'] })。
3.8 算法迁移路径
如果现有系统使用 RS256,希望迁移到 ES256 以获得更优性能,建议采用双算法过渡策略:
- 签发端:同时支持 RS256 和 ES256 两种签名算法,新令牌优先使用 ES256。
- 验签端:配置两个公钥(RSA 公钥 + ECC 公钥),根据 JWT 头中的 alg 字段选择对应公钥验签。
- 灰度期:监控 ES256 令牌的验签成功率,确认无误后逐步下线 RS256 签发。
- 清理:所有令牌过期后,移除 RSA 密钥对和相关代码。
这种平滑迁移方式可避免服务中断,同时逐步降低签名体积和验证延迟。