RS256与ES256签名
在上一章中,我们了解了使用 HMAC(Keyed-Hash Message Authentication Code,密钥散列消息认证码) 的 HS256 签名。HMAC 需要通信双方共享同一个密钥,任何能验证令牌的人也能伪造令牌。当广州飞翔科技(learnto.cn)需要向多个外部合作伙伴分发身份令牌时,共享密钥方案就不再适用。架构师白歌提出:"我们需要一种 一对多(one-to-many) 的签名方案——签发方独自持有私钥,验证方只持有公钥。"
本章将介绍两种非对称签名算法: RS256 (RSA + SHA-256)和 ES256 (ECDSA + P-256 + SHA-256),并结合飞翔科技的实际场景讲解其用法。
RS256:RSA 签名 + SHA-256
RS256 全称 RSASSA-PKCS1-v1_5 using SHA-256 ,即使用 RSA(Rivest–Shamir–Adleman) 算法的签名变体配合 SHA-256(Secure Hash Algorithm 256-bit) 哈希函数。
核心原理
RSA 是一种 非对称密码学(asymmetric cryptography) 算法,它生成一对密钥:
- 私钥(private key) :仅由签发方持有,用于 创建签名(sign) 。
- 公钥(public key) :可以公开分发给任意验证方,用于 验证签名(verify) 。
飞翔科技场景 :白歌在设计联邦身份认证系统时,决定由飞翔科技的认证中心统一签发令牌。各业务线(电商、社区、内容平台)只需部署公钥即可验证用户身份,无需知道私钥。即使某个业务线的服务器被攻破,攻击者也无法伪造新的有效令牌。
使用 OpenSSL 生成 RSA 密钥对
小崔在本地开发环境需要一套测试密钥,他使用 OpenSSL 快速生成:
# 生成 2048 位 RSA 私钥
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
# 从私钥派生对应的公钥
openssl rsa -pubout -in private_key.pem -out public_key.pem
生成的 private_key.pem 和 public_key.pem 都是 PEM 格式的文本文件,可直接复制到代码中使用。
RS256 签名与验证代码
以下示例使用 JavaScript 的 jsonwebtoken 库:
import jwt from 'jsonwebtoken';
const payload = {
sub: "feixiang-user-10086",
name: "赵鸣",
department: "产品部",
role: "content_operator"
};
// 使用私钥签名
const privateRsaKey = `<YOUR-PRIVATE-RSA-KEY>`;
const signed = jwt.sign(payload, privateRsaKey, {
algorithm: 'RS256',
expiresIn: '2h'
});
// 使用公钥验证
const publicRsaKey = `<YOUR-PUBLIC-RSA-KEY>`;
const decoded = jwt.verify(signed, publicRsaKey, {
algorithms: ['RS256'] // 明确指定算法,防止签名剥离攻击
});
console.log(decoded);
安全提示 :
algorithms选项必须显式指定。如果省略,攻击者可能剥离签名、修改头部为alg: none或更弱的算法,从而绕过验证。
ES256:ECDSA 签名 + P-256 + SHA-256
ES256 全称 ECDSA using P-256 and SHA-256 ,即使用 ECDSA(Elliptic Curve Digital Signature Algorithm,椭圆曲线数字签名算法) 配合 P-256 曲线和 SHA-256 哈希函数。
核心原理
ECDSA 与 RSA 一样属于非对称签名算法,也使用私钥签名、公钥验证。但两者的数学基础不同:
- RSA 基于 大整数分解(integer factorization) 难题。
- ECDSA 基于 椭圆曲线离散对数(elliptic curve discrete logarithm) 难题。
在同等安全强度下,ECDSA 的密钥尺寸远小于 RSA。例如,256 位的 ECDSA 密钥与 3072 位的 RSA 密钥提供相近的安全性,但前者的签名结果更短、计算更快,特别适合移动端和物联网设备。
使用 OpenSSL 生成 ECDSA 密钥对
小崔对比测试时,用 OpenSSL 生成 ECDSA 密钥:
# 生成 P-256 曲线私钥(prime256v1 即 JWA 规范中的 P-256)
openssl ecparam -name prime256v1 -genkey -noout -out ecdsa_private_key.pem
# 从私钥派生公钥
openssl ec -in ecdsa_private_key.pem -pubout -out ecdsa_public_key.pem
打开生成的文件可以看到,ECDSA 密钥的文本量明显比 RSA 少得多。
ES256 签名与验证代码
import jwt from 'jsonwebtoken';
const payload = {
sub: "feixiang-user-10010",
name: "杨英",
department: "运营部",
role: "activity_operator"
};
// 使用 ECDSA 私钥签名
const privateEcdsaKey = `<YOUR-PRIVATE-ECDSA-KEY>`;
const signed = jwt.sign(payload, privateEcdsaKey, {
algorithm: 'ES256',
expiresIn: '2h'
});
// 使用 ECDSA 公钥验证
const publicEcdsaKey = `<YOUR-PUBLIC-ECDSA-KEY>`;
const decoded = jwt.verify(signed, publicEcdsaKey, {
algorithms: ['ES256']
});
console.log(decoded);
注意 :ECDSA 签名每次都会产生不同的字节序列(因为引入了随机数),但验证结果始终一致。某些旧版库可能对签名格式有严格要求,需确保使用 DER 编码的 ASN.1 格式。
算法对比:HMAC vs RSA vs ECDSA
小崔在内部技术分享会上,向团队整理了一份三种签名算法的对比表:
| 对比维度 | HS256 (HMAC) | RS256 (RSA) | ES256 (ECDSA) |
|---|---|---|---|
| 密钥类型 | 对称密钥(single secret) | 非对称密钥对(key pair) | 非对称密钥对(key pair) |
| 签名速度 | 快 | 较慢 | 快 |
| 验证速度 | 快 | 较快 | 较快 |
| 签名长度 | 256 位 | 2048 位 | 512 位 |
| 密钥尺寸 | 短(任意字符串) | 长(2048 位约 1.6KB) | 极短(256 位约 32 字节) |
| 分发场景 | 一对一(内部服务) | 一对多(开放平台) | 一对多(移动端/IoT) |
| 安全性假设 | 密钥不泄露 | 大整数分解难题 | 椭圆曲线离散对数难题 |
| 典型用途 | 微服务内部通信 | 联邦身份认证、OAuth | 移动 App、带宽受限环境 |
飞翔科技决策 :
- 白歌为 联邦身份认证中心 选择 RS256 ,因为公钥易于分发给多个合作伙伴,且兼容性好。
- 小崔为 乐途运动 App 的客户端令牌选择 ES256 ,因为移动端网络带宽有限,更短的签名能减少传输开销。
- 李眉在 内部容器网络 的服务间调用中继续使用 HS256 ,因为共享密钥管理简单、性能最优。
选择合适的算法
白歌在架构评审会上总结道:"没有绝对最好的算法,只有最适合场景的算法。"
- 如果你的系统需要 多个验证方 且不能让他们拥有签发能力,选择 RS256 或 ES256 。
- 如果你的部署环境 带宽或存储极度受限 (如物联网设备、小程序),优先选择 ES256 。
- 如果你的服务完全在 可信内部网络 中运行,且追求极致性能, HS256 仍然是一个务实的选择。
无论选择哪种算法,请务必在验证时 显式声明 algorithms 白名单 ,这是防止算法混淆攻击(algorithm confusion attack)的最后一道防线。