JWT 与 OAuth #
什么是 JWT? #
JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息。JWT 是一个紧凑的、URL 安全的方式,用于表示要在双方之间传输的声明(claims)。
text
┌─────────────────────────────────────────────────────────────┐
│ JWT 特点 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ✅ 紧凑 │
│ - 体积小,可通过 URL 传输 │
│ - 可放入 HTTP Header │
│ │
│ ✅ 自包含 │
│ - 包含用户信息 │
│ - 无需查询数据库 │
│ │
│ ✅ 可验证 │
│ - 数字签名保证完整性 │
│ - 可验证发送者身份 │
│ │
│ ✅ 跨语言 │
│ - 所有主流语言都支持 │
│ - 标准化格式 │
│ │
└─────────────────────────────────────────────────────────────┘
JWT 结构 #
三部分组成 #
text
┌─────────────────────────────────────────────────────────────┐
│ JWT 结构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ xxxxx.yyyyy.zzzzz │
│ │ │ │ │
│ │ │ └── Signature(签名) │
│ │ └───────── Payload(载荷) │
│ └───────────────── Header(头部) │
│ │
│ 示例: │
│ eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. │
│ eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0 │
│ IjoxNTE2MjM5MDIyfQ. │
│ SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c │
│ │
└─────────────────────────────────────────────────────────────┘
Header(头部) #
json
{
"alg": "RS256",
"typ": "JWT",
"kid": "key-id-123"
}
字段说明:
| 字段 | 描述 |
|---|---|
| alg | 签名算法(HS256, RS256, ES256 等) |
| typ | 令牌类型,通常为 JWT |
| kid | 密钥标识符(可选) |
Payload(载荷) #
json
{
"iss": "https://auth.example.com",
"sub": "user-123",
"aud": "client-app",
"exp": 1516239022,
"iat": 1516239022,
"nbf": 1516239022,
"jti": "unique-token-id",
"scope": "profile email",
"name": "John Doe",
"email": "john@example.com"
}
标准声明(Registered Claims):
| 声明 | 名称 | 描述 |
|---|---|---|
| iss | Issuer | 签发者 |
| sub | Subject | 主题(用户标识) |
| aud | Audience | 受众(接收方) |
| exp | Expiration Time | 过期时间 |
| iat | Issued At | 签发时间 |
| nbf | Not Before | 生效时间 |
| jti | JWT ID | 唯一标识符 |
Signature(签名) #
text
签名过程:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
或
RS256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
privateKey
)
JWT 签名算法 #
对称算法(HMAC) #
text
┌─────────────────────────────────────────────────────────────┐
│ HMAC 算法 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 算法:HS256, HS384, HS512 │
│ │
│ 特点: │
│ - 使用相同的密钥签名和验证 │
│ - 计算速度快 │
│ - 密钥必须安全分发 │
│ │
│ 适用场景: │
│ - 单一授权服务器 │
│ - 内部服务间通信 │
│ │
│ 风险: │
│ - 密钥泄露后所有令牌都不安全 │
│ - 多个服务需要共享密钥 │
│ │
└─────────────────────────────────────────────────────────────┘
非对称算法(RSA/ECDSA) #
text
┌─────────────────────────────────────────────────────────────┐
│ RSA/ECDSA 算法 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 算法:RS256, RS384, RS512, ES256, ES384, ES512 │
│ │
│ 特点: │
│ - 私钥签名,公钥验证 │
│ - 公钥可以公开分发 │
│ - 计算较慢 │
│ │
│ 适用场景: │
│ - 多个资源服务器 │
│ - 公开的 API │
│ - OpenID Connect │
│ │
│ 优势: │
│ - 资源服务器只需公钥 │
│ - 私钥只在授权服务器 │
│ │
└─────────────────────────────────────────────────────────────┘
JWT 在 OAuth 中的应用 #
作为访问令牌 #
text
┌─────────────────────────────────────────────────────────────┐
│ JWT 作为访问令牌 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 优势: │
│ ✅ 无状态验证 │
│ - 资源服务器无需查询数据库 │
│ - 直接验证签名即可 │
│ │
│ ✅ 自包含信息 │
│ - 包含用户标识 │
│ - 包含权限范围 │
│ - 包含过期时间 │
│ │
│ ✅ 分布式友好 │
│ - 多个资源服务器可独立验证 │
│ - 无需共享会话存储 │
│ │
│ 劣势: │
│ ⚠️ 无法主动撤销 │
│ ⚠️ 令牌体积较大 │
│ ⚠️ 敏感信息不应放入 │
│ │
└─────────────────────────────────────────────────────────────┘
访问令牌示例 #
json
{
"iss": "https://auth.example.com",
"sub": "user-123",
"aud": "https://api.example.com",
"exp": 1516239022,
"iat": 1516239022,
"jti": "unique-token-id",
"client_id": "client-app",
"scope": "profile email read:orders",
"tenant_id": "tenant-001"
}
作为 ID Token(OpenID Connect) #
json
{
"iss": "https://auth.example.com",
"sub": "user-123",
"aud": "client-app",
"exp": 1516239022,
"iat": 1516239022,
"auth_time": 1516239000,
"nonce": "n-0S6_WzA2Mj",
"acr": "urn:mace:incommon:iap:silver",
"amr": ["pwd", "mfa"],
"azp": "client-app",
"name": "John Doe",
"email": "john@example.com",
"email_verified": true,
"picture": "https://example.com/john.jpg"
}
JWT 验证 #
验证步骤 #
text
┌─────────────────────────────────────────────────────────────┐
│ JWT 验证步骤 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 解析 JWT │
│ - 分割 Header、Payload、Signature │
│ - Base64URL 解码 │
│ │
│ 2. 验证签名 │
│ - 获取签名密钥/公钥 │
│ - 重新计算签名 │
│ - 比对签名是否一致 │
│ │
│ 3. 验证声明 │
│ - iss:签发者是否正确 │
│ - aud:受众是否包含自己 │
│ - exp:是否已过期 │
│ - nbf:是否已生效 │
│ - iat:签发时间是否合理 │
│ │
│ 4. 验证其他约束 │
│ - scope:权限是否足够 │
│ - 自定义声明 │
│ │
└─────────────────────────────────────────────────────────────┘
Node.js 验证示例 #
javascript
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
const client = jwksClient({
jwksUri: 'https://auth.example.com/.well-known/jwks.json'
});
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
const signingKey = key.publicKey || key.rsaPublicKey;
callback(null, signingKey);
});
}
async function verifyToken(token) {
return new Promise((resolve, reject) => {
jwt.verify(token, getKey, {
issuer: 'https://auth.example.com',
audience: 'client-app',
algorithms: ['RS256']
}, (err, decoded) => {
if (err) {
reject(err);
} else {
resolve(decoded);
}
});
});
}
const decoded = await verifyToken(accessToken);
console.log(decoded);
Python 验证示例 #
python
import jwt
import requests
from cryptography.hazmat.primitives import serialization
def get_public_key(jwks_url, kid):
jwks = requests.get(jwks_url).json()
for key in jwks['keys']:
if key['kid'] == kid:
return jwt.algorithms.RSAAlgorithm.from_jwk(key)
return None
def verify_token(token, jwks_url, issuer, audience):
header = jwt.get_unverified_header(token)
public_key = get_public_key(jwks_url, header['kid'])
return jwt.decode(
token,
public_key,
algorithms=['RS256'],
issuer=issuer,
audience=audience
)
decoded = verify_token(
access_token,
'https://auth.example.com/.well-known/jwks.json',
'https://auth.example.com',
'client-app'
)
print(decoded)
JWKS(JSON Web Key Set) #
什么是 JWKS? #
text
┌─────────────────────────────────────────────────────────────┐
│ JWKS 说明 │
├─────────────────────────────────────────────────────────────┤
│ │
│ JWKS 是一组公钥的 JSON 表示 │
│ │
│ 端点示例: │
│ https://auth.example.com/.well-known/jwks.json │
│ │
│ 响应示例: │
│ { │
│ "keys": [ │
│ { │
│ "kty": "RSA", │
│ "kid": "key-id-123", │
│ "use": "sig", │
│ "alg": "RS256", │
│ "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQ...", │
│ "e": "AQAB" │
│ } │
│ ] │
│ } │
│ │
│ 字段说明: │
│ - kty:密钥类型(RSA, EC) │
│ - kid:密钥标识符 │
│ - use:用途(sig 签名, enc 加密) │
│ - alg:算法 │
│ - n:RSA 模数 │
│ - e:RSA 指数 │
│ │
└─────────────────────────────────────────────────────────────┘
密钥轮换 #
text
┌─────────────────────────────────────────────────────────────┐
│ 密钥轮换策略 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 为什么需要密钥轮换? │
│ - 降低密钥泄露风险 │
│ - 符合安全合规要求 │
│ - 定期更新最佳实践 │
│ │
│ 轮换流程: │
│ │
│ 1. 生成新密钥 │
│ - 生成新的公私钥对 │
│ - 添加到 JWKS(新 kid) │
│ │
│ 2. 使用新密钥签名 │
│ - 新令牌使用新密钥 │
│ - JWKS 包含新旧密钥 │
│ │
│ 3. 过渡期 │
│ - 旧令牌仍可用旧公钥验证 │
│ - 新令牌用新公钥验证 │
│ │
│ 4. 移除旧密钥 │
│ - 所有旧令牌过期后 │
│ - 从 JWKS 移除旧密钥 │
│ │
└─────────────────────────────────────────────────────────────┘
JWT 安全最佳实践 #
安全建议 #
text
┌─────────────────────────────────────────────────────────────┐
│ JWT 安全最佳实践 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 使用强签名算法 │
│ ✅ RS256, ES256 │
│ ⚠️ HS256(密钥管理困难) │
│ ❌ none, HS256 弱密钥 │
│ │
│ 2. 验证所有声明 │
│ - iss(签发者) │
│ - aud(受众) │
│ - exp(过期时间) │
│ │
│ 3. 不要存储敏感信息 │
│ ❌ 密码 │
│ ❌ 信用卡号 │
│ ❌ 身份证号 │
│ │
│ 4. 使用短期令牌 │
│ - 访问令牌:15分钟-1小时 │
│ - ID Token:短有效期 │
│ │
│ 5. 安全存储 │
│ - 不要存储在 localStorage │
│ - 使用 HttpOnly Cookie │
│ │
│ 6. 实现 Token 撤销机制 │
│ - 黑名单 │
│ - 短有效期 + 刷新令牌 │
│ │
└─────────────────────────────────────────────────────────────┘
常见攻击与防护 #
text
┌─────────────────────────────────────────────────────────────┐
│ JWT 攻击与防护 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 算法混淆攻击 │
│ 攻击:将 RS256 改为 HS256 │
│ 防护:明确指定允许的算法 │
│ │
│ 2. none 算法攻击 │
│ 攻击:使用 alg: none 绕过签名 │
│ 防护:拒绝 none 算法 │
│ │
│ 3. 弱密钥攻击 │
│ 攻击:暴力破解弱密钥 │
│ 防护:使用足够强度的密钥 │
│ │
│ 4. 密钥泄露 │
│ 攻击:获取密钥后伪造令牌 │
│ 防护:安全存储密钥,定期轮换 │
│ │
│ 5. 时间攻击 │
│ 攻击:操纵时间相关声明 │
│ 防护:使用时钟同步,允许合理偏差 │
│ │
└─────────────────────────────────────────────────────────────┘
JWT vs 不透明令牌 #
| 特性 | JWT | 不透明令牌 |
|---|---|---|
| 验证方式 | 本地验证签名 | 查询授权服务器 |
| 信息存储 | 自包含 | 需要查询 |
| 撤销 | 困难 | 容易 |
| 性能 | 高(无网络请求) | 低(需要查询) |
| 体积 | 较大 | 较小 |
| 适用场景 | 分布式系统 | 集中式系统 |
参考令牌 + JWT 混合方案 #
text
┌─────────────────────────────────────────────────────────────┐
│ 混合令牌方案 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 方案: │
│ 1. 授权服务器颁发不透明访问令牌 │
│ 2. 资源服务器使用令牌获取 JWT │
│ 3. 资源服务器缓存 JWT │
│ │
│ 流程: │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 客户端 │────>│ 资源服务 │────>│ 授权服务│ │
│ │ │ │ │ │ │ │
│ │ │ │ 验证令牌 │ │ 返回JWT │ │
│ │ │ │ 缓存JWT │ │ │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ 优势: │
│ ✅ 可以立即撤销 │
│ ✅ 减少授权服务器负载(缓存) │
│ ✅ JWT 信息可用 │
│ │
└─────────────────────────────────────────────────────────────┘
下一步 #
现在你已经了解了 JWT 在 OAuth 中的应用,接下来学习 OpenID Connect,了解如何使用 OAuth 实现身份认证!
最后更新:2026-03-28