第九章:JWT 深入解析
“JWT 不是银弹,但用对了就是利器。”
mindmap
root((JWT))
结构
Header
Payload
Signature
签名算法
HS256
RS256
ES256
EdDSA
安全陷阱
alg none
密钥混淆
无法撤销
最佳实践
短过期
JWKS 轮换
Audience 验证
9.1 JWT 结构
JWT(JSON Web Token)由三部分组成,用点号分隔:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJ1c2VyLTEyMyIsIm5hbWUiOiJXYWx0ZXIiLCJyb2xlcyI6WyJhZG1pbiJdLCJleHAiOjE3MDk1MTA0MDB9.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
│←── Header ──→│←────────── Payload ──────────→│←── Signature ──→│
(Base64URL) (Base64URL) (Base64URL)
Header
{
"alg": "RS256", // 签名算法
"typ": "JWT", // Token 类型
"kid": "key-2024" // 密钥 ID(用于 JWKS 轮换)
}
Payload(Claims)
{
"iss": "https://auth.example.com", // 签发者
"sub": "user-123", // 主体
"aud": "my-api", // 受众
"exp": 1709510400, // 过期时间
"nbf": 1709506800, // 生效时间
"iat": 1709506800, // 签发时间
"jti": "unique-token-id", // Token ID
"name": "Walter", // 自定义 Claim
"roles": ["admin", "editor"] // 自定义 Claim
}
9.2 签名算法选择
算法 |
类型 |
密钥 |
适用场景 |
推荐 |
|---|---|---|---|---|
HS256 |
对称 |
共享密钥 |
单体应用 |
⚠️ 简单场景 |
RS256 |
非对称 |
RSA 密钥对 |
微服务 |
✅ 通用 |
ES256 |
非对称 |
ECDSA P-256 |
高性能 |
✅ 推荐 |
EdDSA |
非对称 |
Ed25519 |
最新 |
✅ 最佳性能 |
import jwt
from cryptography.hazmat.primitives.asymmetric import rsa, ec
from cryptography.hazmat.primitives import serialization
# === RS256 签名 ===
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()
token = jwt.encode(
{"sub": "user-123", "roles": ["admin"], "exp": 1709510400},
private_key,
algorithm="RS256",
headers={"kid": "key-2024"}
)
# 验证(只需要公钥)
decoded = jwt.decode(
token, public_key,
algorithms=["RS256"],
audience="my-api", # 验证 audience
issuer="https://auth.example.com" # 验证 issuer
)
# === ES256 签名(更快、更小) ===
ec_private = ec.generate_private_key(ec.SECP256R1())
ec_token = jwt.encode({"sub": "user-123"}, ec_private, algorithm="ES256")
9.3 JWT 安全陷阱
陷阱 1:alg:none 攻击
# ❌ 攻击者构造无签名的 JWT
malicious_header = {"alg": "none", "typ": "JWT"}
malicious_payload = {"sub": "admin", "roles": ["superadmin"]}
# ✅ 防御:始终指定允许的算法
decoded = jwt.decode(
token, public_key,
algorithms=["RS256"], # 明确指定,拒绝 "none"
)
陷阱 2:密钥混淆攻击
# ❌ 攻击者用 RS256 的公钥作为 HS256 的密钥
# 如果服务端不限制算法,攻击者可以:
# 1. 获取 RS256 公钥(公开的)
# 2. 用公钥作为 HS256 密钥签名
# 3. 服务端用同一个公钥验证 HS256 → 通过!
# ✅ 防御:严格限制算法
decoded = jwt.decode(token, public_key, algorithms=["RS256"]) # 只接受 RS256
陷阱 3:Token 无法撤销
JWT 一旦签发就无法撤销(除非等它过期)。解决方案:
import redis
r = redis.Redis()
def revoke_token(jti: str, exp: int):
"""将 Token 加入黑名单"""
ttl = exp - int(time.time())
if ttl > 0:
r.setex(f"revoked:{jti}", ttl, "1")
def is_token_revoked(jti: str) -> bool:
"""检查 Token 是否已被撤销"""
return r.exists(f"revoked:{jti}")
def verify_token(token: str):
payload = jwt.decode(token, public_key, algorithms=["RS256"])
if is_token_revoked(payload.get("jti", "")):
raise Exception("Token has been revoked")
return payload
9.4 JWKS(JSON Web Key Set)
JWKS 用于发布公钥,支持密钥轮换:
// GET https://auth.example.com/.well-known/jwks.json
{
"keys": [
{
"kty": "RSA",
"kid": "key-2024",
"use": "sig",
"alg": "RS256",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM...",
"e": "AQAB"
},
{
"kty": "RSA",
"kid": "key-2023",
"use": "sig",
"alg": "RS256",
"n": "...",
"e": "AQAB"
}
]
}
from jwt import PyJWKClient
# 自动从 JWKS 端点获取公钥
jwks_client = PyJWKClient("https://auth.example.com/.well-known/jwks.json")
def verify_with_jwks(token: str):
"""使用 JWKS 验证 JWT(支持密钥轮换)"""
signing_key = jwks_client.get_signing_key_from_jwt(token)
return jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience="my-api",
)
9.5 JWT vs Session
维度 |
JWT |
Session |
|---|---|---|
存储位置 |
客户端 |
服务端 |
状态 |
无状态 |
有状态 |
扩展性 |
天然分布式 |
需要共享存储 |
撤销 |
困难(需黑名单) |
简单(删除 Session) |
大小 |
较大(含 Claims) |
小(Session ID) |
适用场景 |
微服务、API |
传统 Web 应用 |
9.6 JWT 最佳实践清单
✅ 使用非对称算法(RS256/ES256)
✅ 设置短过期时间(15分钟-1小时)
✅ 验证所有标准 Claims(iss, aud, exp)
✅ 使用 JWKS 支持密钥轮换
✅ 明确指定允许的算法列表
✅ 包含 jti 用于撤销和防重放
✅ 最小化 Payload(不放敏感数据)
✅ 使用 HTTPS 传输
❌ 不要在 URL 中传递 JWT
❌ 不要在 JWT 中存储密码或密钥
❌ 不要使用 HS256 的公开密钥
❌ 不要忽略 exp 验证
9.7 小结
JWT 由 Header.Payload.Signature 三部分组成
推荐使用 RS256 或 ES256 非对称签名算法
注意安全陷阱:alg:none、密钥混淆、无法撤销
JWKS 支持密钥轮换,是生产环境必备
JWT 适合微服务和 API 场景,Session 适合传统 Web 应用