4. JWT 签名算法对比:HS256、RS256、ES256
签名算法是 JWT 安全性的核心。它决定了 Token 如何被签发、如何被验证,以及整个系统的信任模型。本章深入对比三种主流算法:HMAC 对称签名(HS256)、RSA 非对称签名(RS256)和ECDSA 椭圆曲线签名(ES256),帮助你在架构选型时做出正确决策。
4.1 算法分类与核心原理
JWT 签名算法分为对称和非对称两大类。对称签名使用同一个密钥进行签名和验签;非对称签名使用私钥签名、公钥验签。ECDSA 虽然也属于非对称签名,但其数学基础基于椭圆曲线,密钥更短、性能更高。
| 特性 | HS256(HMAC-SHA256) | RS256(RSA-SHA256) | ES256(ECDSA-P256) |
|---|---|---|---|
| 算法类型 | 对称签名 | 非对称签名 | 非对称签名(椭圆曲线) |
| 密钥数量 | 1 个(共享密钥) | 2 个(私钥 + 公钥) | 2 个(私钥 + 公钥) |
| 签名速度 | 快 | 慢 | 中等 |
| 验签速度 | 快 | 慢 | 快 |
| 密钥长度 | 任意(建议 ≥ 256 bit) | 2048 / 4096 bit | 256 bit |
| 签名长度 | 32 字节 | 256 字节(2048 bit) | 64 字节 |
| 安全性基础 | 哈希碰撞 + 密钥保密 | 大整数分解 | 椭圆曲线离散对数 |
| 典型场景 | 单体应用、内部微服务 | 开放 API、跨组织认证 | 移动端、IoT、高并发 |
选型提示:HS256 实现简单,但密钥泄露风险高;RS256 适合需要分发公钥的场景;ES256 在安全性和性能之间取得最佳平衡,是当前 JWT 签名的推荐选择。
4.2 HS256:对称签名
HS256 使用 HMAC-SHA256 算法,签名和验签使用同一个密钥。服务端必须确保密钥不被泄露,任何持有密钥的客户端都可以伪造 Token。
4.2.1 签名与验签流程
// 签名(服务端)
String token = Jwts.builder()
.setSubject("user123")
.signWith(SignatureAlgorithm.HS256, secretKey.getBytes())
.compact();
// 验签(同一服务端或信任方)
Jws<Claims> claims = Jwts.parser()
.setSigningKey(secretKey.getBytes())
.parseClaimsJws(token);
4.2.2 安全风险
- 密钥分发困难:每个需要验签的服务都必须持有相同的密钥,密钥在传输和存储过程中容易泄露。
- 无法实现非对称信任:第三方服务无法独立验证 Token 来源,必须信任密钥持有方。
- 密钥轮换复杂:更换密钥需要所有依赖方同步更新,容易导致服务中断。
安全警告:在生产环境中,切勿将 HS256 密钥硬编码在代码中或提交到版本控制系统。建议使用密钥管理服务(KMS)或环境变量注入。
4.3 RS256:RSA 非对称签名
RS256 基于 RSA 算法,使用私钥签名、公钥验签。私钥仅由签发服务持有,公钥可以安全地分发给任何需要验证 Token 的服务。
4.3.1 密钥对生成
// 生成 2048 位 RSA 密钥对
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(2048);
KeyPair keyPair = generator.generateKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
PublicKey publicKey = keyPair.getPublic();
4.3.2 签名与验签
// 签名(使用私钥)
String token = Jwts.builder()
.setSubject("user456")
.signWith(SignatureAlgorithm.RS256, privateKey)
.compact();
// 验签(使用公钥)
Jws<Claims> claims = Jwts.parser()
.setSigningKey(publicKey)
.parseClaimsJws(token);
4.3.3 性能考量
RS256 的签名操作比 HS256 慢 10-100 倍,验签操作也相对较慢。在 Token 签发频率高或验签请求量大的场景下,RSA 可能成为性能瓶颈。
4.4 ES256:ECDSA 椭圆曲线签名
ES256 使用 NIST P-256 椭圆曲线(secp256r1),提供与 RSA 2048 相当的安全性,但密钥长度更短、签名更小、计算速度更快。
4.4.1 密钥对生成
// 生成 EC P-256 密钥对
KeyPairGenerator generator = KeyPairGenerator.getInstance("EC");
ECGenParameterSpec ecSpec = new ECGenParameterSpec("secp256r1");
generator.initialize(ecSpec);
KeyPair keyPair = generator.generateKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
PublicKey publicKey = keyPair.getPublic();
4.4.2 签名与验签
// 签名(使用私钥)
String token = Jwts.builder()
.setSubject("user789")
.signWith(SignatureAlgorithm.ES256, privateKey)
.compact();
// 验签(使用公钥)
Jws<Claims> claims = Jwts.parser()
.setSigningKey(publicKey)
.parseClaimsJws(token);
4.4.3 优势总结
- 签名长度短:仅 64 字节,适合 HTTP Header 传输。
- 验签速度快:比 RS256 快 5-10 倍,适合高并发验签场景。
- 密钥管理简单:256 位私钥易于安全存储,公钥体积小便于分发。
4.5 签名算法对比 SVG 图
下图从密钥结构、签名过程和验签过程三个维度对比三种算法的工作机制。
4.6 算法选型决策树
根据实际场景选择签名算法,可以参考以下决策流程:
- 是否所有服务都在同一个信任域内?
- 是 → 考虑 HS256(简单快速,但需做好密钥管理)
- 否 → 进入下一步
- 是否需要向第三方分发公钥?
- 是 → 使用非对称签名(RS256 或 ES256)
- 否 → 考虑 HS256 或内部非对称方案
- Token 签发和验签的并发量是否很高?
- 是 → 优先选择 ES256(验签速度快,签名短)
- 否 → RS256 也可接受,但建议评估性能
- 客户端是否受限(移动端、IoT)?
- 是 → 必须选择 ES256(密钥短、签名小、省电)
- 否 → RS256 或 ES256 均可
4.7 签名长度对网络传输的影响
JWT 通常放在 HTTP Header 中传输,签名长度直接影响请求大小。在微服务架构中,Token 可能被多次传递,累积的带宽开销不容忽视。
| 算法 | 签名长度 | 典型 Token 总大小 | 1000 次请求带宽 |
|---|---|---|---|
| HS256 | 32 字节 | ~200 字节 | ~200 KB |
| RS256(2048 bit) | 256 字节 | ~450 字节 | ~450 KB |
| ES256 | 64 字节 | ~250 字节 | ~250 KB |
实践要点:在网关或 API 网关层面,建议统一使用 ES256 签名,既能保证安全性,又能减少网络开销。如果遗留系统使用 RS256,可以考虑在网关处做签名转换,但会增加延迟。
4.8 安全性对比总结
从安全架构角度,三种算法的安全性对比如下:
- HS256:安全性完全依赖密钥保密。一旦密钥泄露,攻击者可以伪造任意 Token。适合内部网络隔离良好的场景。
- RS256:私钥泄露风险可控(仅签发服务持有),公钥泄露不影响安全。但 RSA 算法本身存在量子计算威胁。
- ES256:提供与 RSA 2048 同等级别的安全性,密钥更短,抗量子计算能力略强于 RSA。当前被认为是 JWT 签名的最佳实践。
安全提醒:无论选择哪种算法,都必须定期轮换密钥。建议设置密钥有效期(如 90 天),并使用自动化工具(如 Vault、AWS KMS)管理密钥生命周期。