mcpField
J
jwt-security

jwt-security

github.com/mindrally安全2871.6k 次安装3 周前更新
代码仓库

关于 jwt-security

面向处理 JWT 身份验证与授权的开发者,提供令牌创建、验证和生命周期管理的安全实践指南,涵盖算法选择、密钥轮换与常见攻击防护。

主要功能

  1. 01签名算法与密钥选择
  2. 02声明校验与令牌验证
  3. 03刷新、撤销与密钥轮换
  4. 04存储和传输安全建议
  5. 05常见漏洞与反模式防护

适用场景

  1. 01设计 JWT 身份验证流程
  2. 02检查服务端令牌校验逻辑
  3. 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');
  });
});

应避免的常见反模式

  1. 使用 JWT 管理会话(Web 应用优先使用服务端会话)
  2. 在 JWT 载荷中存储敏感数据(载荷只是经过编码,并未加密)
  3. 未验证所有声明
  4. 使用弱密钥或硬编码密钥
  5. 未设置令牌过期时间
  6. 未验证便信任算法标头
  7. 未实施刷新令牌轮换
  8. 记录完整令牌

评论

支持 Markdown