起因
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)。
迁移:
- 生成 RSA key pair
- JWKS endpoint 暴露 public key
- token issue 支持双 alg:老 token HS256 + 新 token RS256
- token verify 接受 HS256(kid="legacy")+ RS256
- 等所有老 token expire(30 day)
- 删 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)。
踩过的坑
-
algorithms 没指定:confusion attack。永远
algorithms=['XX']。 -
exp 时区:服务器时间错 → 立刻过期 / 永不过期。NTP sync 必备。
-
public key 写错:copy 时 \n 丢 → 验失败。pem 用 file 加载,
不要在 env var 里塞 multi-line。 -
JWT 当 session:放 admin_panel=True 类敏感权限 → token leak 后
攻击者直接获取。敏感操作必须 DB 二次验证。 -
长 expiry + 无 revoke:30 day expiry JWT 用户改密码后老
token 仍有效。要么短 expiry + refresh,要么 blacklist 表(牺牲
stateless)。
登录后参与评论。