关于 jwt-security
面向处理 JWT 身份验证与授权的开发者,提供令牌创建、验证和生命周期管理的安全实践指南,涵盖算法选择、密钥轮换与常见攻击防护。
主要功能
- 01签名算法与密钥选择
- 02声明校验与令牌验证
- 03刷新、撤销与密钥轮换
- 04存储和传输安全建议
- 05常见漏洞与反模式防护
适用场景
- 01设计 JWT 身份验证流程
- 02检查服务端令牌校验逻辑
- 03规划刷新令牌和撤销机制
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ mindrally/ skills --skill jwt-security安装后在 Claude Code 中输入/jwt-security确认已加载。
详细介绍
JWT 安全
你是 JSON Web Token(JWT)安全实现方面的专家。使用 JWT 进行身份验证和授权时,请遵循以下指南。
核心原则
- JWT 本身并不安全,安全性取决于实现方式
- 始终在服务端验证令牌,即使是内部服务也不例外
- 尽可能使用非对称签名(RS256、ES256)
- 缩短令牌有效期,并实施适当的刷新机制
- 切勿在 JWT 载荷中存储敏感数据
令牌结构
JWT 由三个部分组成:标头、载荷和签名。
header.payload.signature
标头最佳实践
{
"alg": "RS256",
"typ": "JWT",
"kid": "key-identifier-for-rotation"
}
- 始终包含
kid(密钥 ID),以支持密钥轮换 - 显式使用
typ: "JWT" - 切勿接受
alg: "none"
载荷最佳实践
{
"iss": "https://auth.example.com",
"sub": "user-uuid-here",
"aud": "https://api.example.com",
"exp": 1704067200,
"iat": 1704063600,
"nbf": 1704063600,
"jti": "unique-token-id"
}
必需声明:
iss(签发者):创建令牌的主体sub(主题):令牌所代表的主体aud(受众):令牌的预期接收方exp(过期时间):令牌过期的时间iat(签发时间):令牌创建的时间
建议声明:
nbf(生效时间):在此时间之前令牌无效jti(JWT ID):用于撤销令牌的唯一标识符
签名算法选择
推荐:非对称算法
// RS256 - RSA with SHA-256 (most widely supported)
// ES256 - ECDSA with P-256 and SHA-256 (smaller keys)
// EdDSA - Edwards-curve Digital Signature Algorithm (most secure)
const ALLOWED_ALGORITHMS = ['RS256', 'ES256', 'EdDSA'];
必须使用对称算法时
// HS256 - HMAC with SHA-256
// Only use with a strong secret (minimum 256 bits / 32 bytes)
const secret = crypto.randomBytes(64).toString('hex');
创建令牌
使用 RS256(推荐)
const jwt = require('jsonwebtoken');
const fs = require('fs');
const privateKey = fs.readFileSync('private.pem');
function createToken(userId, roles) {
const payload = {
sub: userId,
roles: roles,
// Keep custom claims minimal
};
const options = {
algorithm: 'RS256',
expiresIn: '15m', // Short-lived access tokens
issuer: 'https://auth.example.com',
audience: 'https://api.example.com',
keyid: 'current-key-id',
};
return jwt.sign(payload, privateKey, options);
}
令牌有效期指南
const TOKEN_LIFETIMES = {
accessToken: '15m', // 15 minutes max
refreshToken: '7d', // 7 days with rotation
idToken: '1h', // 1 hour
passwordReset: '15m', // 15 minutes
emailVerification: '24h', // 24 hours
};
验证令牌
完整验证示例
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
// JWKS client for fetching public keys
const client = jwksClient({
jwksUri: 'https://auth.example.com/.well-known/jwks.json',
cache: true,
cacheMaxAge: 600000, // 10 minutes
rateLimit: true,
jwksRequestsPerMinute: 10,
});
async function validateToken(token) {
// 1. Decode header without verification to get kid
const decoded = jwt.decode(token, { complete: true });
if (!decoded) {
throw new Error('Invalid token format');
}
// 2. Validate algorithm against whitelist
if (!ALLOWED_ALGORITHMS.includes(decoded.header.alg)) {
throw new Error(`Algorithm ${decoded.header.alg} not allowed`);
}
// 3. Get signing key
const key = await client.getSigningKey(decoded.header.kid);
const publicKey = key.getPublicKey();
// 4. Verify signature and claims
const verified = jwt.verify(token, publicKey, {
algorithms: ALLOWED_ALGORITHMS, // Whitelist algorithms
issuer: 'https://auth.example.com',
audience: 'https://api.example.com',
clockTolerance: 30, // 30 seconds clock skew tolerance
});
return verified;
}
验证清单
function validateTokenClaims(decoded) {
const now = Math.floor(Date.now() / 1000);
// 1. Check expiration
if (decoded.exp && decoded.exp < now) {
throw new Error('Token expired');
}
// 2. Check not before
if (decoded.nbf && decoded.nbf > now) {
throw new Error('Token not yet valid');
}
// 3. Check issuer
if (decoded.iss !== EXPECTED_ISSUER) {
throw new Error('Invalid issuer');
}
// 4. Check audience
const audiences = Array.isArray(decoded.aud) ? decoded.aud : [decoded.aud];
if (!audiences.includes(EXPECTED_AUDIENCE)) {
throw new Error('Invalid audience');
}
// 5. Check required claims exist
if (!decoded.sub) {
throw new Error('Missing subject claim');
}
return true;
}
需要防范的安全漏洞
1. 算法混淆攻击
// WRONG: Accepting any algorithm
jwt.verify(token, secret); // Vulnerable!
// CORRECT: Whitelist allowed algorithms
jwt.verify(token, key, { algorithms: ['RS256'] });
2. None 算法攻击
// Always reject 'none' algorithm
if (decoded.header.alg === 'none' || decoded.header.alg.toLowerCase() === 'none') {
throw new Error('Algorithm none is not allowed');
}
3. 密钥混淆(RS256 与 HS256)
// When using asymmetric keys, never allow symmetric algorithms
const ASYMMETRIC_ONLY = ['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'EdDSA'];
jwt.verify(token, publicKey, { algorithms: ASYMMETRIC_ONLY });
4. 弱 HMAC 密钥
// Minimum 256-bit (32 byte) secret for HS256
// Minimum 384-bit (48 byte) secret for HS384
// Minimum 512-bit (64 byte) secret for HS512
function generateHmacSecret(algorithm) {
const bits = parseInt(algorithm.slice(2)); // HS256 -> 256
const bytes = bits / 8;
return crypto.randomBytes(Math.max(bytes, 32)).toString('hex');
}
令牌存储
浏览器存储安全
// Best: HttpOnly cookie (requires backend support)
// Server sets:
res.cookie('access_token', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 900000, // 15 minutes
});
// Acceptable: In-memory (lost on refresh)
let accessToken = null;
function setToken(token) {
accessToken = token;
}
// Avoid: localStorage (vulnerable to XSS)
// Avoid: sessionStorage for sensitive tokens
令牌传输
// Always use Authorization header
fetch('/api/resource', {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
// Never put tokens in URLs (logged, cached, visible in history)
// WRONG: /api/resource?token=eyJ...
刷新令牌实现
// Refresh tokens should be:
// 1. Stored securely (httpOnly cookie or secure server-side storage)
// 2. Rotated on each use
// 3. Bound to the client (if possible)
async function refreshAccessToken(refreshToken) {
// Validate refresh token
const decoded = await validateRefreshToken(refreshToken);
// Check if token has been revoked
const isRevoked = await checkTokenRevocation(decoded.jti);
if (isRevoked) {
throw new Error('Refresh token has been revoked');
}
// Generate new tokens
const newAccessToken = createAccessToken(decoded.sub);
const newRefreshToken = createRefreshToken(decoded.sub);
// Revoke old refresh token (rotation)
await revokeToken(decoded.jti);
return { accessToken: newAccessToken, refreshToken: newRefreshToken };
}
令牌撤销
// Maintain a revocation list for early token invalidation
const revokedTokens = new Set(); // Use Redis in production
function revokeToken(jti) {
revokedTokens.add(jti);
}
function isTokenRevoked(jti) {
return revokedTokens.has(jti);
}
// Include revocation check in validation
async function validateToken(token) {
const decoded = jwt.verify(token, key, options);
if (decoded.jti && isTokenRevoked(decoded.jti)) {
throw new Error('Token has been revoked');
}
return decoded;
}
密钥轮换
// Support multiple keys during rotation
const keyStore = {
'key-2024-01': { /* current key */ },
'key-2023-12': { /* previous key, still valid */ },
};
// JWKS endpoint should expose all valid public keys
app.get('/.well-known/jwks.json', (req, res) => {
const keys = Object.entries(keyStore).map(([kid, key]) => ({
kid,
kty: 'RSA',
use: 'sig',
alg: 'RS256',
n: key.publicKey.n,
e: key.publicKey.e,
}));
res.json({ keys });
});
Express 中间件示例
const expressJwt = require('express-jwt');
const jwksRsa = require('jwks-rsa');
const jwtMiddleware = expressJwt({
secret: jwksRsa.expressJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: 'https://auth.example.com/.well-known/jwks.json',
}),
audience: 'https://api.example.com',
issuer: 'https://auth.example.com',
algorithms: ['RS256'],
});
// Protected route
app.get('/api/protected', jwtMiddleware, (req, res) => {
// req.auth contains the decoded token
res.json({ user: req.auth.sub });
});
测试
describe('JWT Validation', () => {
it('should reject expired tokens', async () => {
const expiredToken = createToken({ exp: Math.floor(Date.now() / 1000) - 3600 });
await expect(validateToken(expiredToken)).rejects.toThrow('expired');
});
it('should reject tokens with wrong issuer', async () => {
const wrongIssuer = createToken({ iss: 'https://evil.com' });
await expect(validateToken(wrongIssuer)).rejects.toThrow('issuer');
});
it('should reject none algorithm', async () => {
const noneAlg = 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIn0.';
await expect(validateToken(noneAlg)).rejects.toThrow('algorithm');
});
});
应避免的常见反模式
- 使用 JWT 管理会话(Web 应用优先使用服务端会话)
- 在 JWT 载荷中存储敏感数据(载荷只是经过编码,并未加密)
- 未验证所有声明
- 使用弱密钥或硬编码密钥
- 未设置令牌过期时间
- 未验证便信任算法标头
- 未实施刷新令牌轮换
- 记录完整令牌
评论
支持 Markdown