JWT:HS256 vs RS256 vs EdDSA,啥时候哪个

起因

JWT 签名算法常见三个:

  • HS256:HMAC-SHA256,对称密钥
  • RS256:RSA + SHA256,非对称
  • EdDSA (Ed25519):现代椭圆曲线,非对称

选哪个?默认 HS256 行不行?为啥很多场景非 RS256 不可?

HS256 (HMAC)

import jwt

SECRET = 'shared-secret-key'

# 签
token = jwt.encode({'user_id': 42}, SECRET, algorithm='HS256')

# 验
payload = jwt.decode(token, SECRET, algorithms=['HS256'])

对称:签和验用同一 secret

优势

  • 简单(一个 secret)
  • 性能极好(HMAC 比 RSA 快 100x)
  • token 短

劣势 / 适用边界

  • 任何能验 token 的服务都能签 token → 不能给第三方验
  • 单 monolith 内部用 OK;微服务要严格区分签发方与验证方

适合:单一服务 + 用同一 secret 的自家系统。

RS256 (RSA 非对称)

# 服务端:private key 签
with open('private.pem', 'rb') as f:
    private_key = f.read()
token = jwt.encode({'user_id': 42}, private_key, algorithm='RS256')

# 任何方:public key 验
with open('public.pem', 'rb') as f:
    public_key = f.read()
payload = jwt.decode(token, public_key, algorithms=['RS256'])

非对称:private key 签,public key 验
public key 公开(其它服务 / 客户端能拿到),但拿到也不能伪造 token。

优势

  • 安全分离:auth server 持 private key,resource server 只持 public key
  • public key 可发布(JWKS endpoint)
  • 大组织 / OAuth / SSO 标配

劣势

  • 性能:RSA 签验比 HMAC 慢 100-1000x
  • token 大(RSA 签名 256 字节起)
  • key 管理更复杂

适合:微服务 / OAuth / 第三方需验 token。

EdDSA / Ed25519

token = jwt.encode({'user_id': 42}, ed25519_private_key, algorithm='EdDSA')

非对称,基于 Ed25519 椭圆曲线。
RS256 的现代替代:

RS256 (2048 bit) EdDSA (Ed25519)
签名速度 快(10x+)
验证速度 较快 极快
key size 2048 bit 256 bit
签名 size 256 byte 64 byte
安全性 强(现代设计)

新项目能用 EdDSA 就用,性能 + size 全面优于 RS256。
RS256 仍是 OAuth 兼容性最广(IdP / library 都支持)。

决策

监控你的 token 谁签 / 谁验

单 monolith / 自家服务全套:
  → HS256

跨服务 / 公开 API / 给第三方 / SSO:
  → RS256(兼容性)或 EdDSA(性能)

OAuth provider / IdP:
  → RS256(事实标准)

错配灾难(algorithm confusion attack)

# 危险:alg 不锁定
payload = jwt.decode(token, public_key)    # 不指定 algorithms

攻击者把 token 的 alg header 改成 HS256,用 public key 当 secret 签
(因为 RS256 → HS256 confusion)→ 假 token 通过验证。

正确

payload = jwt.decode(token, public_key, algorithms=['RS256'])
# 指定接受的算法,不让 attacker 选

库默认大多防御此攻击,但永远显式指定 algorithms

key rotation

JWT 没内置 rotation。常见做法:

  • token 包含 kid (key ID) header
  • JWKS endpoint 暴露多个 active public key
  • 验证时根据 kid 选 key
# 假设 JWKS 已 cache
def verify(token):
    header = jwt.get_unverified_header(token)
    kid = header['kid']
    key = jwks_cache[kid]
    return jwt.decode(token, key, algorithms=['RS256'])

旧 key 留 grace period 后删 → 老 token 平滑过渡。

token 内容设计

{
  "iss": "https://auth.example.com",    # issuer
  "sub": "user-12345",                  # subject (user)
  "aud": "https://api.example.com",     # audience (intended verifier)
  "iat": 1716000000,                    # issued at
  "exp": 1716003600,                    # expiry
  "nbf": 1716000000,                    # not before
  "jti": "unique-id",                   # JWT ID(防重放)

  // custom
  "roles": ["admin", "editor"],
  "tenant": "acme"
}

验证时检查:

  • exp 没过
  • aud 是自己
  • iss 是受信任 issuer

很多 lib 自动。

refresh token

JWT 短 expiry(15min - 1h)+ refresh token(长期,不透明 random string)。
access token 过期 → 用 refresh token 换新。

refresh token 存数据库(能撤销);access token 是 stateless JWT。

stateless vs session

JWT 优势:服务端不存 session → 横向扩展无 sticky session 问题。
劣势:撤销难(除非短 expiry + refresh token 模式)。

中小项目其实 session cookie + Redis store 简单 + 够用。
微服务 / 多 IdP / OAuth 才必要 JWT。

不要存敏感数据

# bad
jwt.encode({'password': 'secret123', ...}, ...)

JWT body 未加密只签名。base64decode 立刻看到内容。
不要放:password / API secret / PII。

需要加密 → JWE (JSON Web Encryption),但复杂得多,少用。

实战 case:迁移 HS256 → RS256

老项目 monolith HS256。
拆出 mobile API + 计划开放第三方 API → 需要 RS256(合作伙伴验 token
不该知 secret)。

迁移:

  1. 生成 RSA key pair
  2. JWKS endpoint 暴露 public key
  3. token issue 支持双 alg:老 token HS256 + 新 token RS256
  4. token verify 接受 HS256(kid="legacy")+ RS256
  5. 等所有老 token expire(30 day)
  6. 删 HS256 支持

平滑过渡,无服务中断。

性能数据

签 / 验 100k token / single core:

sign verify
HS256 0.5s 0.5s
RS256 (2048) 80s 5s
EdDSA 3s 1s

签贵很多,验也比 HS256 慢。
高 QPS 验 token 时考虑 cache verified token 或者用 EdDSA。

  • Python: PyJWT / python-jose / authlib
  • Node: jsonwebtoken
  • Go: github.com/golang-jwt/jwt
  • Rust: jsonwebtoken

PyJWT 简单 + 维护好。authlib 大全(含 OAuth)。

踩过的坑

  1. algorithms 没指定:confusion attack。永远 algorithms=['XX']

  2. exp 时区:服务器时间错 → 立刻过期 / 永不过期。NTP sync 必备。

  3. public key 写错:copy 时 \n 丢 → 验失败。pem 用 file 加载,
    不要在 env var 里塞 multi-line。

  4. JWT 当 session:放 admin_panel=True 类敏感权限 → token leak 后
    攻击者直接获取。敏感操作必须 DB 二次验证。

  5. 长 expiry + 无 revoke:30 day expiry JWT 用户改密码后老
    token 仍有效。要么短 expiry + refresh,要么 blacklist 表(牺牲
    stateless)。

精确评价 共 0 人评价
可复现性
可复现 · 0 不可复现 · 0
文风
文风流畅 · 0 文风晦涩 · 0
立场
支持 · 0 反对 · 0

登录后即可对本帖作出评价。

评论区 0 条 · 所有人可在此交流

登录后参与评论。

还没有评论,来说两句。