jwt
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseJWT Core Knowledge
JWT核心知识
Deep Knowledge: Usewith technology:mcp__documentation__fetch_docsfor comprehensive documentation.jwt
深度资料:使用工具,指定technology为mcp__documentation__fetch_docs以获取完整文档。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 skill for third-party authentication
oauth2 - NextAuth.js - Use skill for Next.js authentication
nextauth - Simple internal APIs - API keys might be sufficient
- 基于会话的身份验证 - 使用传统服务器端会话+Cookie方案
- OAuth 2.0流程 - 使用技能处理第三方身份验证
oauth2 - NextAuth.js - 使用技能处理Next.js身份验证
nextauth - 简单内部API - API密钥可能已足够
Best Practices
最佳实践
| Do | Don't |
|---|---|
| Use HTTPS | Store in localStorage (use httpOnly cookies) |
| Short expiry (15m-1h) | Put sensitive data in payload |
| Validate all claims | Use weak secrets |
| Use refresh tokens | Ignore expiration |
| 建议 | 禁忌 |
|---|---|
| 使用HTTPS | 存储在localStorage(使用httpOnly Cookie) |
| 短有效期(15分钟-1小时) | 在载荷中放入敏感数据 |
| 验证所有声明 | 使用弱密钥 |
| 使用刷新令牌 | 忽略过期时间 |
Anti-Patterns
反模式
| Anti-Pattern | Why It's Bad | Correct Approach |
|---|---|---|
| Storing JWT in localStorage | Vulnerable to XSS attacks | Use httpOnly cookies |
| Long-lived access tokens | Security risk if compromised | 15-minute expiry + refresh tokens |
| Weak secrets (< 32 bytes) | Easy to brute force | Use 256-bit random secret |
| Ignoring algorithm verification | Algorithm confusion attacks | Explicitly specify allowed algorithms |
| Putting passwords in payload | Token is base64, not encrypted | Only non-sensitive claims |
| No token revocation | Can't logout users | Implement blacklist or token versioning |
| 反模式 | 危害 | 正确方案 |
|---|---|---|
| 将JWT存储在localStorage | 易受XSS攻击 | 使用httpOnly Cookie |
| 长期有效的访问令牌 | 泄露后存在安全风险 | 15分钟有效期 + 刷新令牌 |
| 弱密钥(<32字节) | 易被暴力破解 | 使用256位随机密钥 |
| 忽略算法验证 | 易受算法混淆攻击 | 明确指定允许的算法 |
| 在载荷中放入密码 | 令牌是base64编码而非加密 | 仅放入非敏感声明 |
| 无令牌撤销机制 | 无法让用户登出 | 实现黑名单或令牌版本控制 |
Quick Troubleshooting
快速故障排查
| Issue | Cause | Solution |
|---|---|---|
| "Invalid signature" | Wrong secret or algorithm | Verify JWT_SECRET matches, check algorithm |
| "Token expired" | exp claim in past | Implement refresh token flow |
| "Missing token" | Authorization header not sent | Check |
| Token not recognized | Malformed token | Verify header.payload.signature format |
| CORS errors with cookies | SameSite/Secure flags | Set sameSite:'strict', secure:true |
| Logout doesn't work | Tokens are stateless | Implement revocation with Redis/DB |
| 问题 | 原因 | 解决方案 |
|---|---|---|
| "Invalid signature" | 密钥或算法错误 | 验证JWT_SECRET是否匹配,检查算法 |
| "Token expired" | exp声明已过期 | 实现刷新令牌流程 |
| "Missing token" | 未发送Authorization头 | 检查是否使用 |
| 令牌无法识别 | 令牌格式错误 | 验证header.payload.signature格式 |
| Cookie引发CORS错误 | SameSite/Secure标志问题 | 设置sameSite:'strict', secure:true |
| 登出不生效 | 令牌是无状态的 | 使用Redis/数据库实现撤销机制 |
Standard Claims
标准声明
| Claim | Purpose |
|---|---|
| Subject (user ID) |
| Issued at |
| Expiration |
| Issuer |
| Audience |
| 声明 | 用途 |
|---|---|
| 主体(用户ID) |
| 签发时间 |
| 过期时间 |
| 签发者 |
| 受众 |
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
监控指标
| Metric | Alert 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