RFC 9068 - JWT Profile for OAuth 2.0 Access Tokens
基本信息
- RFC编号: 9068
- 标题: JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens
- 中文标题: OAuth 2.0访问令牌的JWT配置文件
- 发布日期: 2021年10月
- 状态: PROPOSED STANDARD (提案标准)
- 作者: V. Bertocci (Auth0), B. Campbell (Ping Identity)
摘要 (Abstract)
本规范定义了OAuth 2.0访问令牌使用JWT格式的标准配置文件。它规定了JWT访问令牌的必需声明、推荐声明以及验证要求,使资源服务器能够直接验证和解析访问令牌,无需每次都调用内省端点。
JWT访问令牌概述
为什么需要JWT访问令牌?
传统访问令牌的问题:
不透明令牌 (Opaque Token):
- 格式: 随机字符串 (如: "SlAV32hkKG...XPw")
- 特点: 无法直接解析
- 验证: 必须调用内省端点 (RFC 7662)
问题:
❌ 每次API调用都需要内省 → 额外网络请求
❌ 授权服务器负载高 → 性能瓶颈
❌ 延迟增加 → 用户体验差
❌ 单点故障 → 授权服务器宕机影响所有API
示例流程:
客户端 → [access_token] → 资源服务器
↓
内省端点 ← 授权服务器
↓
响应 (active: true)
↓
验证通过
JWT访问令牌的优势:
JWT令牌:
- 格式: 自包含的JSON Web Token
- 特点: 可以直接验证和解析
- 验证: 使用公钥验证签名
优势:
✓ 无需内省 → 零额外请求
✓ 降低授权服务器负载 → 高性能
✓ 低延迟 → 快速响应
✓ 离线验证 → 授权服务器宕机不影响API
✓ 包含上下文信息 → scope, 用户ID等
示例流程:
客户端 → [JWT access_token] → 资源服务器
↓
1. 验证签名 (使用公钥)
2. 验证过期时间
3. 验证audience
↓
验证通过
↓
从JWT读取scope/用户ID
JWT访问令牌 vs 不透明令牌
对比:
不透明令牌 (Opaque Token):
优点:
+ 可以随时撤销 (服务器端控制)
+ 令牌小 (通常20-40字符)
+ 不泄露信息 (无法解析)
缺点:
- 必须内省验证 (网络开销)
- 授权服务器压力大
- 无法离线验证
适用场景:
- 需要即时撤销的场景
- 高安全要求 (不希望令牌可解析)
- 短期令牌
JWT访问令牌:
优点:
+ 自包含 (无需内省)
+ 高性能 (离线验证)
+ 包含上下文 (scope, claims)
+ 可扩展
缺点:
- 令牌大 (通常100-300+字符)
- 难以撤销 (需要额外机制)
- 信息可见 (base64解码可读)
适用场景:
- 高性能API
- 微服务架构
- 分布式系统
- 需要令牌包含上下文信息
混合方案:
- 短期JWT访问令牌 (如15分钟)
- 长期不透明刷新令牌
→ 结合两者优势
JWT访问令牌格式
必需声明 (Required Claims)
{
"iss": "https://authorization-server.example.com",
"exp": 1639533600,
"aud": "https://api.example.com",
"sub": "user-123",
"client_id": "client-456",
"iat": 1639530000,
"jti": "token-789"
}
1. iss (Issuer - 发行者):
"iss": "https://authorization-server.example.com"
说明:
- 授权服务器的标识符
- 必须是HTTPS URL
- 用于查找验证密钥
验证:
const payload = jwt.decode(token);
if (payload.iss !== expectedIssuer) {
throw new Error('Invalid issuer');
}
2. exp (Expiration Time - 过期时间):
"exp": 1639533600 // Unix时间戳
说明:
- 令牌过期时间
- Unix时间戳 (秒)
- 必须是未来时间
验证:
const now = Math.floor(Date.now() / 1000);
if (payload.exp <= now) {
throw new Error('Token expired');
}
建议有效期:
- 短期: 5-15分钟 (高安全)
- 中期: 1小时 (平衡)
- 长期: 24小时 (低安全,不推荐)
3. aud (Audience - 受众):
"aud": "https://api.example.com"
或
"aud": ["https://api.example.com", "https://api2.example.com"]
说明:
- 令牌的目标资源服务器
- 可以是字符串或字符串数组
- 防止令牌被用于非预期的资源
验证:
const expectedAudience = 'https://api.example.com';
if (Array.isArray(payload.aud)) {
if (!payload.aud.includes(expectedAudience)) {
throw new Error('Invalid audience');
}
} else {
if (payload.aud !== expectedAudience) {
throw new Error('Invalid audience');
}
}
4. sub (Subject - 主体):
"sub": "user-123"
说明:
- 令牌的主体标识符
- 通常是用户ID
- 必须在发行者范围内唯一
- 字符串类型
使用:
// 从令牌获取用户ID
const userId = payload.sub;
const user = await getUserById(userId);
5. client_id (Client Identifier - 客户端标识符):
"client_id": "client-456"
说明:
- 请求令牌的客户端ID
- 用于追踪和审计
- 区分不同客户端的请求
使用:
// 检查客户端权限
const client = await getClient(payload.client_id);
if (!client.hasPermission('write')) {
throw new Error('Client not authorized');
}
6. iat (Issued At - 签发时间):
"iat": 1639530000 // Unix时间戳
说明:
- 令牌的签发时间
- Unix时间戳 (秒)
- 用于防止重放攻击
验证:
const now = Math.floor(Date.now() / 1000);
const maxAge = 3600; // 1小时
if (now - payload.iat > maxAge) {
throw new Error('Token too old');
}
7. jti (JWT ID - JWT标识符):
"jti": "token-789"
说明:
- 令牌的唯一标识符
- 用于防止重放攻击
- 可用于撤销令牌
使用:
// 检查令牌是否被撤销
const isRevoked = await checkRevokedToken(payload.jti);
if (isRevoked) {
throw new Error('Token has been revoked');
}
可选声明 (Optional Claims)
8. scope (Scope - 作用域):
"scope": "read write profile"
说明:
- 授权的权限范围
- 空格分隔的字符串
- 资源服务器据此做权限控制
使用:
const scopes = payload.scope.split(' ');
if (!scopes.includes('write')) {
return res.status(403).json({ error: 'Insufficient scope' });
}
9. 自定义声明:
{
"iss": "https://authorization-server.example.com",
"exp": 1639533600,
"aud": "https://api.example.com",
"sub": "user-123",
"client_id": "client-456",
// 自定义声明
"email": "[email protected]",
"name": "John Doe",
"roles": ["admin", "editor"],
"tenant_id": "tenant-789",
"permissions": ["read:articles", "write:articles"]
}
说明:
- 可以添加任意自定义声明
- 用于传递上下文信息
- 减少数据库查询
注意:
⚠️ 令牌大小限制 (通常< 8KB)
⚠️ 不要包含敏感信息 (令牌可被解码)
⚠️ 考虑网络传输开销
完整JWT示例
完整的JWT访问令牌:
eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6IjEyMyJ9.eyJpc3MiOiJodHRwczovL2F1dGhvcml6YXRpb24tc2VydmVyLmV4YW1wbGUuY29tIiwiZXhwIjoxNjM5NTMzNjAwLCJhdWQiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsInN1YiI6InVzZXItMTIzIiwiY2xpZW50X2lkIjoiY2xpZW50LTQ1NiIsImlhdCI6MTYzOTUzMDAwMCwianRpIjoidG9rZW4tNzg5Iiwic2NvcGUiOiJyZWFkIHdyaXRlIn0.signature
分解:
Header (头部):
{
"alg": "RS256",
"typ": "at+jwt", ← 类型: 访问令牌JWT
"kid": "123"
}
Payload (载荷):
{
"iss": "https://authorization-server.example.com",
"exp": 1639533600,
"aud": "https://api.example.com",
"sub": "user-123",
"client_id": "client-456",
"iat": 1639530000,
"jti": "token-789",
"scope": "read write"
}
Signature (签名):
RSASSA-PKCS1-v1_5 using SHA-256
签名和验证
推荐算法
RFC 9068推荐:
优先推荐:
1. RS256 (RSA with SHA-256)
- 非对称加密
- 公钥验证
- 最常用
2. ES256 (ECDSA with P-256 and SHA-256)
- 非对称加密
- 更短的签名
- 更好的性能
不推荐:
❌ HS256 (HMAC with SHA-256)
- 对称加密
- 需要共享密钥
- 安全性较低 (所有资源服务器需要密钥)
为什么推荐非对称算法?
→ 授权服务器用私钥签名
→ 资源服务器用公钥验证
→ 公钥可以公开分发
→ 无需共享密钥
JWT生成 (授权服务器)
const jose = require('jose');
class JWTAccessTokenIssuer {
constructor(privateKey, issuer, keyId) {
this.privateKey = privateKey;
this.issuer = issuer;
this.keyId = keyId;
}
async issueAccessToken(userId, clientId, audience, scope, expiresIn = 900) {
const now = Math.floor(Date.now() / 1000);
const payload = {
// 必需声明
iss: this.issuer,
exp: now + expiresIn, // 默认15分钟
aud: audience,
sub: userId,
client_id: clientId,
iat: now,
jti: this.generateJti(),
// 可选声明
scope: scope
};
// 生成JWT
const jwt = await new jose.SignJWT(payload)
.setProtectedHeader({
alg: 'RS256',
typ: 'at+jwt', // 重要: 标识为访问令牌
kid: this.keyId
})
.sign(this.privateKey);
return jwt;
}
generateJti() {
return require('crypto').randomBytes(16).toString('hex');
}
}
// 使用示例
const { generateKeyPair } = require('jose');
const { privateKey, publicKey } = await generateKeyPair('RS256');
const issuer = new JWTAccessTokenIssuer(
privateKey,
'https://authorization-server.example.com',
'key-123'
);
const accessToken = await issuer.issueAccessToken(
'user-123', // userId
'client-456', // clientId
'https://api.example.com', // audience
'read write', // scope
900 // 15分钟
);
console.log('Access Token:', accessToken);
JWT验证 (资源服务器)
const jose = require('jose');
class JWTAccessTokenValidator {
constructor(issuer, audience) {
this.issuer = issuer;
this.audience = audience;
this.jwksCache = null;
this.jwksCacheTime = 0;
}
async validate(token) {
try {
// 步骤1: 获取JWKS (公钥集)
const jwks = await this.getJWKS();
// 步骤2: 验证JWT
const { payload, protectedHeader } = await jose.jwtVerify(
token,
jwks,
{
issuer: this.issuer,
audience: this.audience,
typ: 'at+jwt' // 确认类型
}
);
// 步骤3: 额外验证
this.validatePayload(payload);
return {
valid: true,
payload,
header: protectedHeader
};
} catch (err) {
return {
valid: false,
error: err.message
};
}
}
async getJWKS() {
// 缓存JWKS 1小时
const now = Date.now();
if (this.jwksCache && (now - this.jwksCacheTime < 3600000)) {
return this.jwksCache;
}
// 从授权服务器获取JWKS
const jwksUrl = `${this.issuer}/.well-known/jwks.json`;
const jwks = jose.createRemoteJWKSet(new URL(jwksUrl));
this.jwksCache = jwks;
this.jwksCacheTime = now;
return jwks;
}
validatePayload(payload) {
// 检查必需声明
const required = ['iss', 'exp', 'aud', 'sub', 'client_id', 'iat', 'jti'];
for (const claim of required) {
if (!payload[claim]) {
throw new Error(`Missing required claim: ${claim}`);
}
}
// 检查client_id格式
if (typeof payload.client_id !== 'string') {
throw new Error('client_id must be a string');
}
// 可以添加更多自定义验证...
}
// 提取scope
getScopes(payload) {
if (!payload.scope) return [];
return payload.scope.split(' ');
}
// 检查权限
hasScope(payload, requiredScope) {
const scopes = this.getScopes(payload);
return scopes.includes(requiredScope);
}
}
// 使用示例
const validator = new JWTAccessTokenValidator(
'https://authorization-server.example.com',
'https://api.example.com'
);
// 验证访问令牌
const result = await validator.validate(accessToken);
if (result.valid) {
console.log('Token is valid');
console.log('User ID:', result.payload.sub);
console.log('Client ID:', result.payload.client_id);
console.log('Scopes:', result.payload.scope);
} else {
console.error('Token is invalid:', result.error);
}
Express中间件实现
JWT验证中间件
const express = require('express');
const jose = require('jose');
// JWT验证中间件
function jwtAuthMiddleware(options = {}) {
const {
issuer,
audience,
requiredScopes = []
} = options;
const validator = new JWTAccessTokenValidator(issuer, audience);
return async (req, res, next) => {
try {
// 步骤1: 提取令牌
const token = extractToken(req);
if (!token) {
return res.status(401).json({
error: 'invalid_request',
error_description: 'Missing access token'
});
}
// 步骤2: 验证令牌
const result = await validator.validate(token);
if (!result.valid) {
return res.status(401).json({
error: 'invalid_token',
error_description: result.error
});
}
// 步骤3: 检查scope
if (requiredScopes.length > 0) {
const hasAllScopes = requiredScopes.every(scope =>
validator.hasScope(result.payload, scope)
);
if (!hasAllScopes) {
return res.status(403).json({
error: 'insufficient_scope',
error_description: `Required scopes: ${requiredScopes.join(', ')}`
});
}
}
// 步骤4: 附加到请求对象
req.auth = {
token: result.payload,
userId: result.payload.sub,
clientId: result.payload.client_id,
scopes: validator.getScopes(result.payload)
};
next();
} catch (err) {
console.error('Auth middleware error:', err);
res.status(500).json({
error: 'server_error',
error_description: 'Internal server error'
});
}
};
}
// 提取令牌辅助函数
function extractToken(req) {
const authHeader = req.headers.authorization;
if (!authHeader) {
return null;
}
const parts = authHeader.split(' ');
if (parts.length !== 2 || parts[0] !== 'Bearer') {
return null;
}
return parts[1];
}
// 使用示例
const app = express();
// 配置JWT验证
const jwtAuth = jwtAuthMiddleware({
issuer: 'https://authorization-server.example.com',
audience: 'https://api.example.com'
});
// 公开端点 (无需认证)
app.get('/api/public', (req, res) => {
res.json({ message: 'Public endpoint' });
});
// 受保护端点 (需要认证)
app.get('/api/protected', jwtAuth, (req, res) => {
res.json({
message: 'Protected endpoint',
user: req.auth.userId,
client: req.auth.clientId
});
});
// 需要特定scope的端点
app.post('/api/articles',
jwtAuthMiddleware({
issuer: 'https://authorization-server.example.com',
audience: 'https://api.example.com',
requiredScopes: ['write', 'articles']
}),
(req, res) => {
res.json({
message: 'Article created',
author: req.auth.userId
});
}
);
// 动态scope检查
app.delete('/api/articles/:id', jwtAuth, async (req, res) => {
const article = await getArticle(req.params.id);
// 检查权限: 管理员或作者本人
const isAdmin = req.auth.scopes.includes('admin');
const isAuthor = article.authorId === req.auth.userId;
if (!isAdmin && !isAuthor) {
return res.status(403).json({
error: 'insufficient_permissions',
error_description: 'You can only delete your own articles'
});
}
await deleteArticle(req.params.id);
res.json({ message: 'Article deleted' });
});
app.listen(3000, () => {
console.log('API server running on http://localhost:3000');
});
Scope验证装饰器
// 高级scope验证
class ScopeValidator {
// 要求所有scope (AND逻辑)
static requireAll(...scopes) {
return (req, res, next) => {
const hasAll = scopes.every(scope =>
req.auth.scopes.includes(scope)
);
if (!hasAll) {
return res.status(403).json({
error: 'insufficient_scope',
error_description: `Required scopes: ${scopes.join(' AND ')}`
});
}
next();
};
}
// 要求任一scope (OR逻辑)
static requireAny(...scopes) {
return (req, res, next) => {
const hasAny = scopes.some(scope =>
req.auth.scopes.includes(scope)
);
if (!hasAny) {
return res.status(403).json({
error: 'insufficient_scope',
error_description: `Required scopes: ${scopes.join(' OR ')}`
});
}
next();
};
}
// 复杂表达式
static requireExpression(expression) {
return (req, res, next) => {
const scopes = req.auth.scopes;
// 示例: "(read AND write) OR admin"
const result = evaluateExpression(expression, scopes);
if (!result) {
return res.status(403).json({
error: 'insufficient_scope',
error_description: `Required: ${expression}`
});
}
next();
};
}
}
// 使用
app.get('/api/data',
jwtAuth,
ScopeValidator.requireAll('read', 'data'),
(req, res) => {
// 需要 read AND data
}
);
app.post('/api/admin/users',
jwtAuth,
ScopeValidator.requireAny('admin', 'super_admin'),
(req, res) => {
// 需要 admin OR super_admin
}
);
令牌撤销策略
问题和解决方案
JWT的撤销难题:
问题:
JWT是自包含的,资源服务器不查询授权服务器
→ 无法即时撤销令牌
场景:
1. 用户登出
2. 密码被修改
3. 权限被撤销
4. 安全事件
解决方案:
方案1: 短期令牌
// 发行短期访问令牌 (5-15分钟)
const accessToken = await issuer.issueAccessToken(
userId,
clientId,
audience,
scope,
900 // 15分钟
);
优点:
✓ 简单实现
✓ 自动过期快
✓ 撤销窗口小
缺点:
- 需要频繁刷新
- 用户体验受影响
方案2: 撤销列表 (Revocation List)
class TokenRevocationList {
constructor() {
this.revokedTokens = new Set();
// 生产环境应使用Redis等分布式缓存
}
// 撤销令牌
revoke(jti, expiresAt) {
this.revokedTokens.add(jti);
// 设置过期时间 (令牌过期后自动清理)
setTimeout(() => {
this.revokedTokens.delete(jti);
}, (expiresAt * 1000) - Date.now());
}
// 检查是否被撤销
isRevoked(jti) {
return this.revokedTokens.has(jti);
}
}
const revocationList = new TokenRevocationList();
// 在验证中间件中检查
async function checkRevocation(req, res, next) {
const jti = req.auth.token.jti;
if (revocationList.isRevoked(jti)) {
return res.status(401).json({
error: 'invalid_token',
error_description: 'Token has been revoked'
});
}
next();
}
// 使用
app.use('/api', jwtAuth, checkRevocation);
方案3: 版本号/时间戳
// 用户表添加字段
{
userId: '123',
tokenVersion: 5, // 每次登出/修改密码时增加
// 或
tokensInvalidBefore: 1639530000 // 此时间之前的令牌无效
}
// 生成令牌时包含版本号
const accessToken = await issuer.issueAccessToken(
userId,
clientId,
audience,
scope,
900,
{ token_version: user.tokenVersion } // 自定义声明
);
// 验证时检查
async function checkTokenVersion(req, res, next) {
const userId = req.auth.userId;
const tokenVersion = req.auth.token.token_version;
const user = await getUserById(userId);
if (tokenVersion < user.tokenVersion) {
return res.status(401).json({
error: 'invalid_token',
error_description: 'Token version outdated'
});
}
next();
}
方案4: 混合方案 (推荐)
// 结合多种策略
class HybridRevocationStrategy {
constructor() {
this.shortTermRevocations = new Set(); // 近期撤销 (Redis)
this.userVersions = new Map(); // 用户令牌版本 (数据库)
}
async isValid(payload) {
// 检查1: JTI是否在撤销列表
if (this.shortTermRevocations.has(payload.jti)) {
return false;
}
// 检查2: 令牌版本
const userVersion = await this.getUserTokenVersion(payload.sub);
if (payload.token_version < userVersion) {
return false;
}
// 检查3: 时间戳
const invalidBefore = await this.getUserTokensInvalidBefore(payload.sub);
if (payload.iat < invalidBefore) {
return false;
}
return true;
}
async revokeToken(jti, expiresAt) {
// 短期撤销 (直到令牌过期)
this.shortTermRevocations.add(jti);
setTimeout(() => {
this.shortTermRevocations.delete(jti);
}, (expiresAt * 1000) - Date.now());
}
async revokeAllUserTokens(userId) {
// 增加用户令牌版本
const currentVersion = await this.getUserTokenVersion(userId);
await this.setUserTokenVersion(userId, currentVersion + 1);
}
}
性能优化
JWKS缓存
class JWKSCache {
constructor(jwksUrl, cacheDuration = 3600000) {
this.jwksUrl = jwksUrl;
this.cacheDuration = cacheDuration;
this.cache = null;
this.cacheTime = 0;
}
async getJWKS() {
const now = Date.now();
// 检查缓存
if (this.cache && (now - this.cacheTime < this.cacheDuration)) {
return this.cache;
}
// 获取新JWKS
try {
const response = await fetch(this.jwksUrl);
const jwks = await response.json();
this.cache = jwks;
this.cacheTime = now;
return jwks;
} catch (err) {
// 如果获取失败但有旧缓存,继续使用
if (this.cache) {
console.warn('JWKS fetch failed, using cached version');
return this.cache;
}
throw err;
}
}
invalidate() {
this.cache = null;
this.cacheTime = 0;
}
}
验证结果缓存
class TokenValidationCache {
constructor(cacheDuration = 60000) { // 1分钟
this.cache = new Map();
this.cacheDuration = cacheDuration;
}
get(token) {
const entry = this.cache.get(token);
if (!entry) {
return null;
}
if (Date.now() > entry.expiresAt) {
this.cache.delete(token);
return null;
}
return entry.result;
}
set(token, result) {
this.cache.set(token, {
result,
expiresAt: Date.now() + this.cacheDuration
});
// 定期清理
this.cleanup();
}
cleanup() {
const now = Date.now();
for (const [token, entry] of this.cache.entries()) {
if (now > entry.expiresAt) {
this.cache.delete(token);
}
}
}
}
// 使用
const validationCache = new TokenValidationCache();
async function validateWithCache(token, validator) {
// 检查缓存
let result = validationCache.get(token);
if (!result) {
// 验证令牌
result = await validator.validate(token);
// 只缓存有效的令牌
if (result.valid) {
validationCache.set(token, result);
}
}
return result;
}
最佳实践
1. 令牌大小优化
// ❌ 不好: 令牌太大
{
"iss": "https://authorization-server.example.com",
"exp": 1639533600,
"aud": "https://api.example.com",
"sub": "user-123",
"client_id": "client-456",
"iat": 1639530000,
"jti": "token-789",
"scope": "read write",
// 太多自定义声明
"email": "[email protected]",
"full_name": "John Robert Smith Junior",
"profile_picture": "https://cdn.example.com/users/123/profile/large.jpg",
"address": {
"street": "123 Main Street",
"city": "New York",
"state": "NY",
"zip": "10001",
"country": "USA"
},
"preferences": { /* 大量数据 */ }
}
// 结果: 令牌 > 2KB
// ✓ 好: 令牌精简
{
"iss": "https://auth.example.com", // 短域名
"exp": 1639533600,
"aud": "https://api.example.com",
"sub": "user-123",
"client_id": "client-456",
"iat": 1639530000,
"jti": "token-789",
"scope": "read write",
// 只包含必要的自定义声明
"roles": ["admin"]
}
// 结果: 令牌 < 500字节
策略:
1. 使用短的发行者URL
2. 最小化自定义声明
3. 将大量数据存在服务器端,令牌只包含引用
4. 使用角色而非详细权限
2. 安全性
// ✓ 正确的安全配置
const securityConfig = {
// 短期令牌
accessTokenTTL: 900, // 15分钟
// 使用非对称算法
algorithm: 'RS256',
// typ头部
tokenType: 'at+jwt',
// 具体的audience
audience: 'https://api.example.com', // 不要用通配符
// 强制HTTPS
requireHTTPS: true,
// 时钟偏移容忍
clockTolerance: 60 // 1分钟
};
// ❌ 不安全的配置
const insecureConfig = {
accessTokenTTL: 86400, // 24小时 - 太长!
algorithm: 'HS256', // 对称算法 - 不推荐!
tokenType: 'JWT', // 不明确
audience: '*', // 通配符 - 危险!
requireHTTPS: false // HTTP - 不安全!
};
3. 错误处理
// 完善的错误处理
class JWTErrorHandler {
static handle(err, req, res, next) {
if (err.name === 'JWTExpired') {
return res.status(401).json({
error: 'invalid_token',
error_description: 'The access token expired',
error_uri: 'https://docs.example.com/errors/token-expired'
});
}
if (err.name === 'JWTClaimValidationFailed') {
return res.status(401).json({
error: 'invalid_token',
error_description: `Token validation failed: ${err.claim}`,
error_uri: 'https://docs.example.com/errors/invalid-token'
});
}
if (err.name === 'JWSSignatureVerificationFailed') {
return res.status(401).json({
error: 'invalid_token',
error_description: 'Token signature verification failed'
});
}
// 通用错误
console.error('JWT error:', err);
res.status(401).json({
error: 'invalid_token',
error_description: 'Token validation failed'
});
}
}
app.use(JWTErrorHandler.handle);
总结
关键要点
✓ JWT访问令牌是自包含的
✓ 无需内省 → 高性能
✓ 离线验证 → 可扩展
✓ 包含上下文 → 减少数据库查询
✓ 标准化格式 → 互操作性
必需声明:
- iss, exp, aud, sub, client_id, iat, jti
推荐实践:
✓ 使用RS256或ES256算法
✓ 短期令牌 (5-15分钟)
✓ typ头部设为"at+jwt"
✓ 具体的audience
✓ 精简令牌大小
✓ 实现撤销策略
性能优化:
✓ JWKS缓存
✓ 验证结果缓存
✓ 异步验证
安全考虑:
✓ 强制HTTPS
✓ 验证所有声明
✓ 实现撤销机制
✓ 监控异常令牌
参考文献
相关RFC:
- [RFC 9068] JWT Profile for OAuth 2.0 Access Tokens ← 本文档
- [RFC 7519] JSON Web Token (JWT)
- [RFC 7515] JSON Web Signature (JWS)
- [RFC 6749] OAuth 2.0 Authorization Framework
- [RFC 7662] OAuth 2.0 Token Introspection
相关库:
- jose - JavaScript JWT库
- jsonwebtoken - Node.js JWT库
总结: RFC 9068定义了OAuth 2.0访问令牌使用JWT的标准格式,通过自包含和离线验证,大幅提升API性能和可扩展性。结合短期令牌和合适的撤销策略,可以在保持高性能的同时确保安全性!