JWT 签名算法选型:HS256、RS256、ES256、PS256 到底用哪个

· 约 5 分钟 🪪 JWT 签发

JWT 的”签名算法”看似只是 header 里一个 alg 字段,但选错会直接影响安全性、性能、密钥管理复杂度。HS256、RS256、ES256、PS256——每个都对应着不同的密钥体系、性能曲线和威胁模型。这篇按”决策树”的方式讲清各算法的取舍,让你能在 5 分钟内为新项目锁定算法。

一张算法对照表

算法类型密钥体系签名长度速度典型场景
HS256HMAC-SHA256对称(共享 secret)32 字节极快单体应用、Webhook
HS384HMAC-SHA384对称48 字节极快同上,需更高安全
HS512HMAC-SHA512对称64 字节同上,最高强度
RS256RSA-PKCS1-SHA256非对称 RSA256 字节微服务、OAuth
RS384/RS512同上非对称 RSA384/512 字节高合规场景
ES256ECDSA-P256-SHA256非对称 EC64 字节现代 OAuth/OIDC
ES384ECDSA-P384-SHA384非对称 EC96 字节中快政府/军工
PS256RSA-PSS-SHA256非对称 RSA256 字节FIPS 合规
EdDSAEd25519非对称 EC64 字节最快最新规范

光看这张表难判断——下面按场景拆解。

单方签发自验:HS256

特征:发 token 和验 token 都是同一个服务(或同一组对等服务),不需要把验证权下放给第三方。

典型场景

  • 单体 Web 应用:Express/Django/Rails 直接发 token、验 token
  • Webhook 验签:GitHub、Stripe、Shopify 给你一个 secret,发 webhook 时签名,你用同一 secret 验
  • 内部 API:网关签发,业务服务用 secret 验
  • IoT 设备 token:设备和服务器预共享 key

为什么是 HS256

  • 密钥就一个 secret,无需 PKI 基建
  • 签发和验证都是 HMAC,单核每秒数十万次,几乎零成本
  • 部署只是把 secret 塞进环境变量

注意事项

  • secret 至少 32 字节,从 crypto.randomBytes(32)
  • 任何持有 secret 的服务都能伪造任意 token——所以 secret 不要广撒
  • 密钥泄露 = 全员被盗,必须立即轮换

微服务、跨团队:RS256 / ES256

特征:签发服务和验证服务不是同一个,需要”签发权与验证权分离”。

典型场景

  • 鉴权中心签发 token,订单服务、支付服务、物流服务各自验证
  • B2B 平台:你签发 token,第三方业务方持你的公钥验证
  • 单点登录 SSO:IdP(Identity Provider)签,多个 SP(Service Provider)验

为什么不能用 HS256:HS256 的 secret 给到验证服务后,那个服务就也能签——任何下游都能伪造 token,安全模型崩塌。

RS256 vs ES256

维度RS256ES256
公钥大小≈270 字节≈64 字节
私钥大小≈1700 字节≈120 字节
签名长度256 字节64 字节
token 总长短 30-40%
签名性能中等
验证性能中等
算法成熟度极高
移动端友好一般

实务

  • 新项目优先 ES256:token 短、JWKS 体积小、移动端流量友好
  • 已有 RS256 不必紧急切:RSA-2048 仍然安全,切换成本不低
  • OAuth/OIDC 标准化场景:双方都接受时优先 ES256

高合规场景:PS256

特征:受 FIPS 140-3、PCI DSS 4.0、政府/军工等监管约束。

为什么是 PS256:PS256 用 RSA-PSS,是 RSA 签名的现代化版本,引入随机 salt,有可证明安全的密码学证明。RS256 用的 PKCS#1 v1.5 在工程上没有已知漏洞,但密码学界认为 PSS”更现代”。

实际取舍

  • 没有合规要求时,PS256 性能略低于 RS256,没有显著优势
  • 库支持比 RS256 略弱——部分老库(早期 Java、PHP 库)不支持
  • 切换成本和 RS256 差不多,但收益主要是”满足审计”而非真实安全提升

结论:除非合规审计要求,否则跳过 PS256,直接用 RS256 或 ES256。

最新规范:EdDSA(Ed25519)

特征:RFC 8037 定义的 JWT 新算法,基于 Ed25519 椭圆曲线。

优势

  • 签名速度比 ECDSA 快 2-3 倍
  • 公钥/签名都只 32/64 字节
  • 不需要 RFC 6979 那样的确定性 nonce 处理——曲线设计本身确定性
  • 抗侧信道攻击设计

劣势

  • JWT 库支持不齐——node-jsonwebtoken 直到 v9 才完整支持
  • 部分网关、CDN(Cloudflare、AWS API Gateway)不识别
  • 监管合规未必接受

实务:技术选型激进的团队可以用 EdDSA;保守团队用 ES256 即可,差距不大。

密钥长度指引

算法推荐密钥长度最低
HS25632 字节(256 位)32 字节
HS38448 字节48 字节
HS51264 字节64 字节
RS256/PS256RSA-2048RSA-2048
RS384/PS384RSA-3072RSA-2048
ES256EC P-256EC P-256
ES384EC P-384EC P-384
EdDSAEd25519Ed25519

HS 密钥openssl rand -base64 32crypto.randomBytes(32).toString('base64') 生成,不要自己拍字符串。

RSA 密钥

openssl genrsa -out priv.pem 2048
openssl rsa -in priv.pem -pubout -out pub.pem

EC 密钥(P-256):

openssl ecparam -genkey -name prime256v1 -noout -out priv.pem
openssl ec -in priv.pem -pubout -out pub.pem

时效字段:iat / exp / nbf 怎么设

签发时至少带这三个字段:

{
  "iat": 1714200000,
  "exp": 1714201800,
  "nbf": 1714200000,
  "sub": "user_12345",
  "role": "user"
}
字段含义推荐值
iat签发时间(Unix 秒)当前时间
exp过期时间iat + 15 分钟(access)/ + 7 天(refresh)
nbf生效时间等于 iat 或省略
jtitoken 唯一 ID用于黑名单/吊销
iss签发方你的服务标识
aud预期接收方验证端要校验
sub主体(通常 user_id)必填

短 access + 长 refresh:access 15 分钟到期,refresh 7-30 天,业务服务只验 access,refresh 只在 /refresh 端点用。这样 access 被盗的时间窗口短,refresh 可以在服务端立即吊销。

验签时的硬规则:算法白名单

无论用什么算法,验签代码必须写死白名单

// ❌ 错:信 header 里的 alg
jwt.verify(token, key);

// ✅ 对:白名单
jwt.verify(token, key, { algorithms: ['RS256'] });

不写死会被 alg confusion 攻击:攻击者把 RS256 token 的 header 改成 alg: HS256,用你公开的 RSA 公钥当 HMAC secret 重签——服务端按 HS256 验证,公钥正好等于 secret,验证通过。

更狠的 alg: none 攻击:攻击者把 alg 改成 "none"、签名段留空、payload 随便改——老库直接放行。

白名单是 JWT 验签的第一防线,比选哪个算法更重要

选型决策树

1. 签发方和验证方是同一个吗?
   ├─ 是  → HS256(密钥 ≥ 32 字节)
   └─ 否  → 继续
2. 有 FIPS / 政府合规要求吗?
   ├─ 是  → PS256 / ES384
   └─ 否  → 继续
3. 移动端 / 流量敏感?
   ├─ 是  → ES256
   └─ 否  → RS256(成熟)/ ES256(更现代)

绝大多数场景,HS256 + RS256 + ES256 三个就够覆盖。再往外的算法属于”特定合规需求”或”前沿尝鲜”,按需引入即可。

❓ 常见问题

HS256 和 RS256,怎么选?

单方签发自验用 HS256,多方分发用 RS256HS256(HMAC-SHA256):(1) 对称密钥——签发和验证用同一个 secret;(2) 速度极快——1ms 内能签上千次;(3) 部署简单——只一份 secret 配进环境变量;(4) 缺点——任何能验证 token 的服务都能伪造 token,无法做"签发权与验证权分离"。RS256(RSA-SHA256):(1) 非对称——私钥签、公钥验;(2) 慢得多——RSA-2048 签名 ≈5ms、验证 ≈0.1ms;(3) 公钥可以公开分发——多个微服务持有公钥但只有签发服务有私钥;(4) 缺点——密钥更长(RSA-2048 私钥约 1.7KB),token 更大。判断:(1) 单体应用、内部 API、Webhook ——HS256;(2) 微服务、跨团队、第三方接入——RS256;(3) 已有 OAuth/OIDC 基础设施——通常已经是 RS256/ES256;(4) IoT / 嵌入式低算力——优先 HS256,CPU 撑不住 RSA。

ES256 比 RS256 好在哪里?为什么 OAuth 新规范偏爱它?

ES256 是 ECDSA + P-256 + SHA-256,主要优势是密钥小、签名小、性能不差对比 RS256:(1) 公钥——RSA-2048 公钥约 270 字节,EC P-256 公钥约 64 字节;(2) 签名长度——RS256 签名 256 字节,ES256 签名 64 字节;(3) 生成速度——签名 ECDSA 比 RSA-2048 快约 5-10 倍;(4) 验证速度——RSA 公钥验证略快,ECDSA 验证略慢,但都在毫秒级;(5) token 整体小 30-40%——网络传输、Cookie 存储更省。OAuth/OIDC 偏爱原因:(1) JWKS 文件更小,下载快;(2) 移动端 / IoT 场景节省字节;(3) NIST 推荐曲线(P-256/P-384)密码学评级高;(4) 与现代 TLS 1.3、Web Push、WebAuthn 的密钥体系统一。唯一缺点:ECDSA 签名强随机性要求——签名时若 nonce 重复或可预测,私钥可被反推(PS3 案例)。所有合规库自动用 RFC 6979 的确定性 nonce,不要自己实现签名。

HS256 的 secret 多长才够?设个短的会怎样?

至少 32 字节高熵随机为什么 32 字节:(1) HMAC-SHA256 的设计安全强度上限是 256 位(即 32 字节);(2) NIST SP 800-107 明确"HMAC key 不应短于哈希输出长度";(3) RFC 7518 也写了"the key MUST have a size equal to or greater than the hash output"。短 secret 的真实风险:(1) 用 your-256-bit-secretsecretmysecret 这种字典词——攻击者拿到一个 token,离线暴力破解可能秒出,伪造任意身份;(2) 用 16 字节随机——理论安全但已不是 SHA-256 的全强度;(3) 用密码学弱随机(Math.random()、UUID v1)——熵不够,可被预测。正确生成方式openssl rand -base64 32crypto.randomBytes(32).toString("base64")secrets.token_urlsafe(32)不要自己拍脑袋编轮换:HS 密钥泄露后无法补救,必须立即轮换并要求所有客户端重新登录——所以密钥管理服务(KMS / Vault)+ 定期轮换是 HS256 的最低门槛。

PS256 比 RS256 强在哪里?要不要切?

PS256 = RSA-PSS + SHA-256,是 RSA 签名的现代化版本PKCS#1 v1.5(RS256 用的)vs PSS:(1) PKCS#1 v1.5 是确定性签名——同一消息每次签出一样的结果;(2) PSS 引入随机 salt——每次签出的结果不同,但都能验证通过;(3) PSS 有可证明安全(provable security)的形式化证明,PKCS#1 v1.5 没有;(4) NIST 推荐新系统用 PSS。实际差异:(1) 现实中 RS256 没有已知可利用攻击——纯密码学层面 PSS 更"现代",但工程上两者都安全;(2) PSS 比 PKCS#1 v1.5 慢一点(多算 salt 和 mask);(3) 库支持——RS256 几乎所有 JWT 库都支持,PS256 部分老库不支持;(4) 标准要求——FIPS 140-3、PCI DSS 4.0 等推荐 PSS。实务:(1) 全新项目可以选 PS256;(2) 已有 RS256 不需要紧急切;(3) 如果监管要求 FIPS / 合规审计可能要求 PSS——按合规需求决定;(4) 多数公司直接跳过 PS256,从 RS256 切到 ES256,因为 EC 优势更明显。

iat、exp、nbf 该设多长?JWT 不能太短也不能太长?

短 access + 长 refresh 是标准模式iat(Issued At) 签发时间——直接当前秒数(Math.floor(Date.now()/1000)),别用毫秒或字符串。exp(Expiration) 过期时间——access token 15 分钟,refresh token 7-30 天nbf(Not Before) 生效时间——通常等于 iat 或省略;只在"延迟生效 token"(如预约访问)才设置。为什么 access 要短:(1) JWT 无状态——服务端无法主动作废,过期是唯一保护;(2) 被窃后窗口越短损失越小;(3) 短期 token 让"用户在 A 设备改了权限,B 设备 15 分钟后就生效"成为可能。为什么 refresh 要长:(1) 用户体验——长 refresh 才能"7 天免登录";(2) refresh 必须存服务端可吊销列表,被盗能立即作废;(3) refresh 端点要严格防 CSRF + 防爆破。典型坑:(1) exp 设成 1 年——等于把无状态 JWT 当 session cookie 用,被盗后只能干等;(2) iat 没设——服务端无法判断"这个 token 是不是改密码前签发的";(3) 时钟漂移——服务器之间相差 30 秒就有 token "未生效"或"已过期",验证端要留 leeway。

JWT 算法字段 alg 在哪个位置?为什么强调"不能信 header 的 alg"?

alg 在 JWT header 的第一段——{"alg":"HS256","typ":"JWT"} base64 后是 token 的第一段。经典攻击 alg confusion:(1) 服务端代码 jwt.verify(token, secret) 不传 algorithms 白名单;(2) 库默认根据 header 里的 alg 选验签算法;(3) 攻击者把原本 RS256 的 token 改成 alg: HS256,把 RSA 公钥(公开可得)拿去当 HMAC secret 计算签名;(4) 服务端按 HS256 验证,secret 正好就是公钥——验证通过。alg none 攻击:(1) RFC 7519 允许 alg: "none"("未签名");(2) 老版本库见到 none 直接通过;(3) 攻击者把 alg 改成 none、签名段留空、payload 改成 role: admin——服务端放行。正确写法:(1) 验签时强制写死算法——jwt.verify(token, key, { algorithms: ["RS256"] });(2) 永远不要从 header 里读取 alg 后选择算法;(3) 验签前看 alg 是否在白名单,否则直接拒。

🪪 打开 JWT 签发 HS/RS/ES/PS 全套·PEM/JWK 私钥·iat/exp 快捷·WebCrypto 本地签名密钥不上传