jwt

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

JWT Core Knowledge

JWT核心知识

Deep Knowledge: Use
mcp__documentation__fetch_docs
with technology:
jwt
for comprehensive documentation.
深度资料:使用
mcp__documentation__fetch_docs
工具,指定technology为
jwt
以获取完整文档。

Token Structure

令牌结构

header.payload.signature

Header: { "alg": "HS256", "typ": "JWT" }
Payload: { "sub": "1234", "name": "John", "iat": 1516239022 }
Signature: HMACSHA256(base64(header) + "." + base64(payload), secret)
header.payload.signature

Header: { "alg": "HS256", "typ": "JWT" }
Payload: { "sub": "1234", "name": "John", "iat": 1516239022 }
Signature: HMACSHA256(base64(header) + "." + base64(payload), secret)

Node.js Implementation

Node.js实现

typescript
import jwt from 'jsonwebtoken';

const SECRET = process.env.JWT_SECRET!;

// Generate token
function generateToken(user: User): string {
  return jwt.sign(
    { sub: user.id, email: user.email },
    SECRET,
    { expiresIn: '1h' }
  );
}

// Verify token
function verifyToken(token: string): JwtPayload {
  return jwt.verify(token, SECRET) as JwtPayload;
}

// Refresh token pattern
function generateRefreshToken(user: User): string {
  return jwt.sign(
    { sub: user.id, type: 'refresh' },
    SECRET,
    { expiresIn: '7d' }
  );
}
typescript
import jwt from 'jsonwebtoken';

const SECRET = process.env.JWT_SECRET!;

// 生成令牌
function generateToken(user: User): string {
  return jwt.sign(
    { sub: user.id, email: user.email },
    SECRET,
    { expiresIn: '1h' }
  );
}

// 验证令牌
function verifyToken(token: string): JwtPayload {
  return jwt.verify(token, SECRET) as JwtPayload;
}

// 刷新令牌模式
function generateRefreshToken(user: User): string {
  return jwt.sign(
    { sub: user.id, type: 'refresh' },
    SECRET,
    { expiresIn: '7d' }
  );
}

Middleware

中间件

typescript
const authenticate = (req, res, next) => {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing token' });
  }

  const token = authHeader.split(' ')[1];
  try {
    req.user = verifyToken(token);
    next();
  } catch (err) {
    res.status(401).json({ error: 'Invalid token' });
  }
};
typescript
const authenticate = (req, res, next) => {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: '缺少令牌' });
  }

  const token = authHeader.split(' ')[1];
  try {
    req.user = verifyToken(token);
    next();
  } catch (err) {
    res.status(401).json({ error: '无效令牌' });
  }
};

When NOT to Use This Skill

不适用本技能的场景

  • Session-based authentication - Use traditional server-side sessions with cookies
  • OAuth 2.0 flows - Use
    oauth2
    skill for third-party authentication
  • NextAuth.js - Use
    nextauth
    skill for Next.js authentication
  • Simple internal APIs - API keys might be sufficient
  • 基于会话的身份验证 - 使用传统服务器端会话+Cookie方案
  • OAuth 2.0流程 - 使用
    oauth2
    技能处理第三方身份验证
  • NextAuth.js - 使用
    nextauth
    技能处理Next.js身份验证
  • 简单内部API - API密钥可能已足够

Best Practices

最佳实践

DoDon't
Use HTTPSStore in localStorage (use httpOnly cookies)
Short expiry (15m-1h)Put sensitive data in payload
Validate all claimsUse weak secrets
Use refresh tokensIgnore expiration
建议禁忌
使用HTTPS存储在localStorage(使用httpOnly Cookie)
短有效期(15分钟-1小时)在载荷中放入敏感数据
验证所有声明使用弱密钥
使用刷新令牌忽略过期时间

Anti-Patterns

反模式

Anti-PatternWhy It's BadCorrect Approach
Storing JWT in localStorageVulnerable to XSS attacksUse httpOnly cookies
Long-lived access tokensSecurity risk if compromised15-minute expiry + refresh tokens
Weak secrets (< 32 bytes)Easy to brute forceUse 256-bit random secret
Ignoring algorithm verificationAlgorithm confusion attacksExplicitly specify allowed algorithms
Putting passwords in payloadToken is base64, not encryptedOnly non-sensitive claims
No token revocationCan't logout usersImplement blacklist or token versioning
反模式危害正确方案
将JWT存储在localStorage易受XSS攻击使用httpOnly Cookie
长期有效的访问令牌泄露后存在安全风险15分钟有效期 + 刷新令牌
弱密钥(<32字节)易被暴力破解使用256位随机密钥
忽略算法验证易受算法混淆攻击明确指定允许的算法
在载荷中放入密码令牌是base64编码而非加密仅放入非敏感声明
无令牌撤销机制无法让用户登出实现黑名单或令牌版本控制

Quick Troubleshooting

快速故障排查

IssueCauseSolution
"Invalid signature"Wrong secret or algorithmVerify JWT_SECRET matches, check algorithm
"Token expired"exp claim in pastImplement refresh token flow
"Missing token"Authorization header not sentCheck
Authorization: Bearer <token>
Token not recognizedMalformed tokenVerify header.payload.signature format
CORS errors with cookiesSameSite/Secure flagsSet sameSite:'strict', secure:true
Logout doesn't workTokens are statelessImplement revocation with Redis/DB
问题原因解决方案
"Invalid signature"密钥或算法错误验证JWT_SECRET是否匹配,检查算法
"Token expired"exp声明已过期实现刷新令牌流程
"Missing token"未发送Authorization头检查是否使用
Authorization: Bearer <token>
格式
令牌无法识别令牌格式错误验证header.payload.signature格式
Cookie引发CORS错误SameSite/Secure标志问题设置sameSite:'strict', secure:true
登出不生效令牌是无状态的使用Redis/数据库实现撤销机制

Standard Claims

标准声明

ClaimPurpose
sub
Subject (user ID)
iat
Issued at
exp
Expiration
iss
Issuer
aud
Audience
声明用途
sub
主体(用户ID)
iat
签发时间
exp
过期时间
iss
签发者
aud
受众

Production Readiness

生产环境就绪

Security Configuration

安全配置

typescript
// Use asymmetric keys (RS256) for production
import * as jose from 'jose';

// Generate key pair (run once, store securely)
// openssl genrsa -out private.pem 2048
// openssl rsa -in private.pem -pubout -out public.pem

const privateKey = await jose.importPKCS8(
  process.env.JWT_PRIVATE_KEY!,
  'RS256'
);
const publicKey = await jose.importSPKI(
  process.env.JWT_PUBLIC_KEY!,
  'RS256'
);

// Sign token
async function generateToken(user: User): Promise<string> {
  return new jose.SignJWT({
    sub: user.id,
    email: user.email,
  })
    .setProtectedHeader({ alg: 'RS256', typ: 'JWT' })
    .setIssuedAt()
    .setIssuer(process.env.JWT_ISSUER!)
    .setAudience(process.env.JWT_AUDIENCE!)
    .setExpirationTime('15m')  // Short-lived access token
    .sign(privateKey);
}

// Verify token
async function verifyToken(token: string): Promise<jose.JWTPayload> {
  const { payload } = await jose.jwtVerify(token, publicKey, {
    issuer: process.env.JWT_ISSUER!,
    audience: process.env.JWT_AUDIENCE!,
  });
  return payload;
}
typescript
// 生产环境使用非对称密钥(RS256)
import * as jose from 'jose';

// 生成密钥对(仅运行一次,安全存储)
// openssl genrsa -out private.pem 2048
// openssl rsa -in private.pem -pubout -out public.pem

const privateKey = await jose.importPKCS8(
  process.env.JWT_PRIVATE_KEY!,
  'RS256'
);
const publicKey = await jose.importSPKI(
  process.env.JWT_PUBLIC_KEY!,
  'RS256'
);

// 签名令牌
async function generateToken(user: User): Promise<string> {
  return new jose.SignJWT({
    sub: user.id,
    email: user.email,
  })
    .setProtectedHeader({ alg: 'RS256', typ: 'JWT' })
    .setIssuedAt()
    .setIssuer(process.env.JWT_ISSUER!)
    .setAudience(process.env.JWT_AUDIENCE!)
    .setExpirationTime('15m')  // 短有效期访问令牌
    .sign(privateKey);
}

// 验证令牌
async function verifyToken(token: string): Promise<jose.JWTPayload> {
  const { payload } = await jose.jwtVerify(token, publicKey, {
    issuer: process.env.JWT_ISSUER!,
    audience: process.env.JWT_AUDIENCE!,
  });
  return payload;
}

Secure Token Storage

安全令牌存储

typescript
// Server-side: HttpOnly cookie for access token
res.cookie('access_token', token, {
  httpOnly: true,     // Prevents XSS access
  secure: true,       // HTTPS only
  sameSite: 'strict', // CSRF protection
  maxAge: 15 * 60 * 1000, // 15 minutes
  path: '/',
});

// Refresh token in separate cookie
res.cookie('refresh_token', refreshToken, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict',
  maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
  path: '/api/auth/refresh', // Only sent to refresh endpoint
});
typescript
// 服务端:使用HttpOnly Cookie存储访问令牌
res.cookie('access_token', token, {
  httpOnly: true,     // 防止XSS访问
  secure: true,       // 仅HTTPS
  sameSite: 'strict', // CSRF防护
  maxAge: 15 * 60 * 1000, // 15分钟
  path: '/',
});

// 刷新令牌存储在独立Cookie中
res.cookie('refresh_token', refreshToken, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict',
  maxAge: 7 * 24 * 60 * 60 * 1000, // 7天
  path: '/api/auth/refresh', // 仅发送到刷新端点
});

Token Rotation & Revocation

令牌轮换与撤销

typescript
// Refresh token rotation
async function refreshTokens(refreshToken: string) {
  // Verify refresh token
  const payload = await verifyRefreshToken(refreshToken);

  // Check if refresh token is in blacklist (revoked)
  if (await isTokenRevoked(refreshToken)) {
    throw new Error('Token revoked');
  }

  // Revoke old refresh token
  await revokeToken(refreshToken);

  // Generate new tokens
  const user = await db.users.findUnique({ where: { id: payload.sub } });
  return {
    accessToken: await generateToken(user),
    refreshToken: await generateRefreshToken(user),
  };
}

// Token revocation with Redis
async function revokeToken(token: string): Promise<void> {
  const payload = await jose.decodeJwt(token);
  const ttl = payload.exp! - Math.floor(Date.now() / 1000);
  if (ttl > 0) {
    await redis.set(`revoked:${token}`, '1', 'EX', ttl);
  }
}

// Logout: revoke all user tokens
async function logoutAll(userId: string): Promise<void> {
  // Increment user's token version, invalidating all existing tokens
  await db.users.update({
    where: { id: userId },
    data: { tokenVersion: { increment: 1 } },
  });
}
typescript
// 刷新令牌轮换
async function refreshTokens(refreshToken: string) {
  // 验证刷新令牌
  const payload = await verifyRefreshToken(refreshToken);

  // 检查刷新令牌是否在黑名单中(已撤销)
  if (await isTokenRevoked(refreshToken)) {
    throw new Error('令牌已撤销');
  }

  // 撤销旧刷新令牌
  await revokeToken(refreshToken);

  // 生成新令牌
  const user = await db.users.findUnique({ where: { id: payload.sub } });
  return {
    accessToken: await generateToken(user),
    refreshToken: await generateRefreshToken(user),
  };
}

// 使用Redis实现令牌撤销
async function revokeToken(token: string): Promise<void> {
  const payload = await jose.decodeJwt(token);
  const ttl = payload.exp! - Math.floor(Date.now() / 1000);
  if (ttl > 0) {
    await redis.set(`revoked:${token}`, '1', 'EX', ttl);
  }
}

// 登出:撤销用户所有令牌
async function logoutAll(userId: string): Promise<void> {
  // 增加用户的令牌版本,使所有现有令牌失效
  await db.users.update({
    where: { id: userId },
    data: { tokenVersion: { increment: 1 } },
  });
}

Algorithm Security

算法安全

typescript
// NEVER allow 'none' algorithm
// ALWAYS specify allowed algorithms explicitly
const { payload } = await jose.jwtVerify(token, publicKey, {
  algorithms: ['RS256'], // Only allow RS256
  issuer: process.env.JWT_ISSUER!,
  audience: process.env.JWT_AUDIENCE!,
});

// Validate token type to prevent token confusion
if (payload.type !== 'access') {
  throw new Error('Invalid token type');
}
typescript
// 绝不允许使用'none'算法
// 始终明确指定允许的算法
const { payload } = await jose.jwtVerify(token, publicKey, {
  algorithms: ['RS256'], // 仅允许RS256
  issuer: process.env.JWT_ISSUER!,
  audience: process.env.JWT_AUDIENCE!,
});

// 验证令牌类型以防止令牌混淆
if (payload.type !== 'access') {
  throw new Error('无效令牌类型');
}

Monitoring Metrics

监控指标

MetricAlert Threshold
Token verification failures> 100/min
Refresh token reuse attempts> 10/min
Expired token requests> 500/min
Invalid signature errors> 50/min
指标告警阈值
令牌验证失败次数>100次/分钟
刷新令牌重复使用尝试>10次/分钟
过期令牌请求>500次/分钟
无效签名错误>50次/分钟

Claims Validation

声明验证

typescript
async function validateTokenClaims(payload: jose.JWTPayload): Promise<void> {
  // Check required claims
  if (!payload.sub || !payload.iat || !payload.exp) {
    throw new Error('Missing required claims');
  }

  // Check user still exists and is active
  const user = await db.users.findUnique({ where: { id: payload.sub } });
  if (!user || !user.isActive) {
    throw new Error('User not found or inactive');
  }

  // Check token version (for logout-all functionality)
  if (payload.tokenVersion !== user.tokenVersion) {
    throw new Error('Token invalidated');
  }
}
typescript
async function validateTokenClaims(payload: jose.JWTPayload): Promise<void> {
  // 检查必填声明
  if (!payload.sub || !payload.iat || !payload.exp) {
    throw new Error('缺少必填声明');
  }

  // 检查用户是否存在且处于活跃状态
  const user = await db.users.findUnique({ where: { id: payload.sub } });
  if (!user || !user.isActive) {
    throw new Error('用户不存在或已禁用');
  }

  // 检查令牌版本(用于全设备登出功能)
  if (payload.tokenVersion !== user.tokenVersion) {
    throw new Error('令牌已失效');
  }
}

Checklist

检查清单

  • Use RS256 (asymmetric) in production
  • Short access token expiry (15 minutes)
  • Refresh tokens with rotation
  • HttpOnly cookies (not localStorage)
  • Secure + SameSite cookie flags
  • Token revocation mechanism
  • Validate issuer and audience
  • Specify allowed algorithms explicitly
  • Include token version for logout-all
  • Monitor verification failures
  • Rate limit token endpoints
  • 生产环境使用RS256(非对称)算法
  • 访问令牌短有效期(15分钟)
  • 刷新令牌轮换机制
  • 使用HttpOnly Cookie(而非localStorage)
  • 设置Secure + SameSite Cookie标志
  • 令牌撤销机制
  • 验证签发者和受众
  • 明确指定允许的算法
  • 包含令牌版本以支持全设备登出
  • 监控验证失败情况
  • 对令牌端点做限流

Reference Documentation

参考文档

  • Refresh Tokens
  • Security Best Practices
  • Refresh Tokens
  • Security Best Practices