JWT 签名算法:HS256 与 RS256 的选择及实现
签名算法是 JWT 的「防伪封印」。你用的密钥对不对、用哪种算法签,直接决定了 能不能安全地交给第三方。今天咱们就把 HS256 和 RS256 这两个主流选项掰开揉碎,看看它们各有什么脾气。
1. 对称签名 HS256 — 简单但风险集中
HS256 用的是同一个密钥来做签名和验签。我最早接触 JWT 时就是用的它,因为实现起来真的很简单——一把钥匙开一把锁。
- 算法本质:HMAC-SHA256,密钥只有一份,签发和验证都用它。
- 优点:代码量少,性能高,适合内部系统对等互信的场景。
- 缺点:密钥一旦泄露,任何人都能伪造合法 。
⚠️ 注意:HS256 的密钥长度至少要 32 个字节(256 bit)。我用过太多次 6 位密码那种烂密钥生成方案,结果被人秒猜出来——教训深刻。
举个实际场景:两个微服务在同一内网,互相调用的 用 HS256 签,完全没有问题。但如果你想给第三方应用或 App 端发 ,那把密钥就等于公开了——坏人拿到它就能随意冒充你发 。
2. 非对称签名 RS256 — 安全解耦
RS256 使用 RSA 公私钥对。私钥只有服务器知道,用来签名;公钥可以公开给任何人,用来验证。说白了,你只需要保护好私钥就行。
- 算法本质:RSA-SHA256,签名用私钥,验签用公钥。
- 优点:私钥永不流出,安全性高;公钥可以放在 .well-known/jwks.json 上,客户端自动拉取。
- 缺点:签名和验签速度比 HS256 慢一个数量级,但现代硬件上完全可接受。
💡 我个人的习惯:对外暴露的 API 网关全部用 RS256,这样合作方的客户端只需拿到公钥就能验证 真伪,不用拿到我的密钥。
我记得有一次对接某银行的开放平台,对方坚持用 RS256,说因为他们的风控要求私钥必须埋在 HSM(硬件安全模块)里,不能用软件存。嗯,这才是真正的安全思维。
3. HS256 vs RS256 对比
| 维度 | HS256(对称) | RS256(非对称) |
|---|---|---|
| 密钥数量 | 1 个共享密钥 | 私钥 + 公钥 |
| 签名性能 | 快(约 200μs/签名) | 慢(约 5ms/签名) |
| 验签性能 | 同签名速度 | 比签名略快,但仍是 HS 的几十倍 |
| 密钥泄露影响 | 完全沦陷,任何人可发假 | 公钥泄露无影响,私钥泄露则危险 |
| 适用场景 | 内部微服务、临时共享 | 对外 API、跨组织协同、移动端 |
4. 动手实现:Python 代码示例
我用 PyJWT 库,一行都不多写。先看 HS256:
import jwt
# HS256 签名
secret = b"my-very-secure-key-at-least-32-bytes-long"
payload = {"user_id": 1001, "role": "admin"}
_hs = jwt.encode(payload, secret, algorithm="HS256")
print("HS256 :", _hs)
# 验证
decoded = jwt.decode(_hs, secret, algorithms=["HS256"])
print("Decoded:", decoded)
再来 RS256,需要先生成 RSA 密钥对。一般用 openssl 生成,然后加载:
import jwt
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend
# 生成 RSA 密钥对(生产环境应该用文件加载)
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend()
)
# 提取 PEM 格式
private_pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption()
)
public_key = private_key.public_key()
public_pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
# RS256 签名
payload = {"sub": "1234567890", "name": "Alice"}
_rs = jwt.encode(payload, private_pem, algorithm="RS256")
print("RS256 :", _rs)
# 使用公钥验证
decoded_rs = jwt.decode(_rs, public_pem, algorithms=["RS256"])
print("RS256 Decoded:", decoded_rs)
📌 关键点:RS256 的签名和验签使用的不是同一个对象。签名只能用私钥,验签只能用公钥。弄反了会直接抛出
InvalidKeyError。
我遇到过一个坑:有个同事把公钥文件路径传到签名函数里,结果 Python 报错说「不能对公钥签名」。嗯,这其实是很多人第一次搞非对称时容易踩的坑。
5. 避坑指南与选择建议
用哪个,不是拍脑袋决定的。我总结了三个问句,问完就有答案:
- 你的 会被公网上的任意客户端验证吗? → 选 RS256。
- 你能保证密钥只在你自己手里,不传给任何人吗? → 可以选 HS256,但必须用够强的密钥。
- 你的客户端需要离线验证签名吗? → 必须 RS256,因为公钥可以提前分发。
我曾经在一个项目里坚持用 HS256 做对外认证,结果被安全审计打回,要求改成 RS256 + JWKS 端点。从那以后,只要涉及跨安全域,我都会默认 RS256。
⚠️ 算法替换的坑:千万不要允许同一个 JWT 同时支持两种算法。攻击者可以构造一个用 HS256 签的 ,然后强制指定算法为 HS256,且公钥能当 HS256 的密钥用?不行,但历史上出现过案例(alg=none 攻击)。所以代码里必须固定
algorithms=["RS256"],不要留个列表让用户选。
最后放一张流程图,把两种算法的签名验证流程画出来,帮大家记清楚: