JWT Token安全实践
约 1505 字大约 5 分钟
jwtsecurity
2025-08-10
概述
JSON Web Token(JWT)是一种紧凑的、自包含的令牌格式,广泛用于 API 认证和授权。JWT 的便捷性使其极受欢迎,但不当使用会引入严重的安全漏洞。本文将深入分析 JWT 的结构、签名算法选择、常见漏洞及防御措施。
JWT 结构
JWT 由三部分组成,用点号分隔:header.payload.signature
Header(头部)
{
"alg": "RS256", // 签名算法
"typ": "JWT", // 令牌类型
"kid": "key-id-1" // 密钥标识(用于密钥轮换)
}Payload(载荷)
{
"iss": "https://auth.example.com", // 签发者
"sub": "user-uuid-123", // 主体(用户标识)
"aud": "https://api.example.com", // 受众
"exp": 1700000000, // 过期时间 (Unix timestamp)
"iat": 1699996400, // 签发时间
"nbf": 1699996400, // 生效时间
"jti": "unique-token-id", // JWT ID(防重放)
"roles": ["admin"], // 自定义声明
"scope": "read write" // 权限范围
}签名算法选择
HS256 (HMAC + SHA-256) — 对称签名
import jwt
SECRET_KEY = "your-256-bit-secret-key-here-minimum-32-bytes!"
# 签发
token = jwt.encode(
{"sub": "user123", "exp": datetime.utcnow() + timedelta(hours=1)},
SECRET_KEY,
algorithm="HS256"
)
# 验证
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])适用场景:单体应用或微服务共享密钥。风险在于所有持有密钥的服务都能签发令牌。
RS256 (RSA + SHA-256) — 非对称签名
import jwt
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
# 生成密钥对
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()
private_pem = private_key.private_bytes(
serialization.Encoding.PEM,
serialization.PrivateFormat.PKCS8,
serialization.NoEncryption()
)
public_pem = public_key.public_bytes(
serialization.Encoding.PEM,
serialization.PublicFormat.SubjectPublicKeyInfo
)
# 签发(仅认证服务持有私钥)
token = jwt.encode(
{"sub": "user123", "iss": "auth-service"},
private_pem,
algorithm="RS256"
)
# 验证(所有服务持有公钥即可)
payload = jwt.decode(token, public_pem, algorithms=["RS256"])ES256 (ECDSA + P-256) — 推荐
from cryptography.hazmat.primitives.asymmetric import ec
private_key = ec.generate_private_key(ec.SECP256R1())
# ES256 签名更短(64字节 vs RSA 256字节),性能更好
token = jwt.encode(payload, private_key, algorithm="ES256")算法对比
| 算法 | 类型 | 签名长度 | 性能 | 推荐度 |
|---|---|---|---|---|
| HS256 | 对称 | 32 bytes | 最快 | 单体应用 |
| RS256 | 非对称 | 256 bytes | 较慢 | 微服务 |
| ES256 | 非对称 | 64 bytes | 快 | 推荐首选 |
| EdDSA | 非对称 | 64 bytes | 快 | 新项目推荐 |
常见安全漏洞
漏洞 1: alg:none 攻击
防御方法:
# 错误:让 JWT 库自动选择算法
payload = jwt.decode(token, key) # 危险!
# 正确:显式指定允许的算法列表
payload = jwt.decode(token, key, algorithms=["RS256"]) # 安全
# 确保你使用的 JWT 库默认拒绝 "none" 算法漏洞 2: HS256/RS256 混淆攻击
如果服务端使用 RS256(公钥验证),攻击者可将 header 的 alg 改为 HS256,然后用公钥(公开信息)作为 HMAC 密钥签名。
# 攻击者利用公开的 RSA 公钥
public_key_bytes = open("public_key.pem", "rb").read()
# 篡改 alg 为 HS256,用 RSA 公钥作为 HMAC 密钥
forged = jwt.encode({"sub": "admin"}, public_key_bytes, algorithm="HS256")
# 防御:服务端永远不要同时允许 HS256 和 RS256
payload = jwt.decode(token, rsa_public_key, algorithms=["RS256"]) # 只允许 RS256漏洞 3: 弱密钥
# 错误:密钥太短或可预测
jwt.encode(payload, "secret", algorithm="HS256") # 可暴力破解
jwt.encode(payload, "your-company-name", algorithm="HS256") # 可字典攻击
# 正确:使用足够长度的随机密钥
import secrets
secret_key = secrets.token_hex(32) # 256-bit 随机密钥漏洞 4: 缺少关键声明验证
# 必须验证的声明
payload = jwt.decode(
token,
key,
algorithms=["RS256"],
options={
"require": ["exp", "iss", "sub", "aud"], # 要求必须存在
"verify_exp": True, # 验证过期时间
"verify_iss": True, # 验证签发者
"verify_aud": True, # 验证受众
},
issuer="https://auth.example.com",
audience="https://api.example.com",
)Refresh Token 轮换
Token 吊销策略
JWT 是无状态的,一旦签发就无法直接撤销。常见的吊销方案:
import redis
r = redis.Redis()
# 方案 1: 黑名单(适合吊销量少的情况)
def revoke_token(jti: str, exp: int):
ttl = exp - int(time.time())
if ttl > 0:
r.setex(f"blacklist:{jti}", ttl, "revoked")
def is_token_revoked(jti: str) -> bool:
return r.exists(f"blacklist:{jti}")
# 方案 2: 版本号(适合"踢出用户"场景)
def revoke_all_user_tokens(user_id: str):
r.incr(f"token_version:{user_id}")
def verify_token_version(user_id: str, token_version: int) -> bool:
current = int(r.get(f"token_version:{user_id}") or 0)
return token_version >= current
# 方案 3: 短生命周期 access_token + refresh_token 轮换
# access_token 有效期 15 分钟,自然过期即失效安全配置清单
from datetime import datetime, timedelta
def create_secure_token(user_id: str, roles: list) -> str:
now = datetime.utcnow()
payload = {
"iss": "https://auth.example.com", # 签发者
"sub": user_id, # 用户标识
"aud": "https://api.example.com", # 目标受众
"exp": now + timedelta(minutes=15), # 短有效期
"iat": now, # 签发时间
"nbf": now, # 生效时间
"jti": str(uuid.uuid4()), # 唯一标识(防重放)
"roles": roles, # 最小权限声明
# 不要存放敏感信息:密码、信用卡号等
}
return jwt.encode(payload, private_key, algorithm="ES256",
headers={"kid": "current-key-id"})最佳实践总结
- 使用非对称算法(ES256/RS256),认证服务签发,其他服务用公钥验证
- 显式指定允许的算法列表,绝不信任 JWT header 中的 alg 字段
- HS256 密钥至少 256 位随机值,不要使用可猜测的字符串
- access_token 有效期控制在 15 分钟以内,配合 refresh_token 轮换
- 验证所有关键声明:exp、iss、aud、sub
- 不要在 payload 中存放敏感数据,JWT payload 只是 Base64 编码,不是加密
- 实现 token 吊销机制:黑名单或版本号方案
- 使用 kid 支持密钥轮换,避免一次性更换密钥导致所有 token 失效
贡献者
更新日志
2026/3/14 13:09
查看所有更新日志
9f6c2-feat: organize wiki content and refresh site setup于