Skip to main content

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

相关库:


总结: RFC 9068定义了OAuth 2.0访问令牌使用JWT的标准格式,通过自包含和离线验证,大幅提升API性能和可扩展性。结合短期令牌和合适的撤销策略,可以在保持高性能的同时确保安全性!