DevToolBox免费
博客

JWT安全最佳实践:算法选择、过期策略、轮换和攻击防御

14分钟作者 DevToolBox

JWT 安全最佳实践:算法选择、过期、轮换与攻击防御

JSON Web Token 无处不在——但大多数实现存在严重安全漏洞。本指南涵盖算法选择(为何 RS256 优于 HS256)、Token 过期与轮换策略、Refresh Token 模式以及如何抵御算法混淆、"none" 算法绕过和密钥暴力破解等顶级 JWT 攻击。

JWT 是什么?它如何工作?

JWT 是一个 Base64URL 编码的字符串,由三个点分隔的部分组成:头部(算法 + 类型)、载荷(声明)和签名。服务端用密钥或私钥签名 Token;客户端每次请求时发送它,服务端验证签名以确认真实性而无需数据库查找。

算法选择:HS256 vs RS256 vs ES256

最常见的 JWT 安全错误是在分布式系统中使用 HS256。HS256 要求每个验证 Token 的服务共享同一个密钥——若一个服务被攻破,所有 Token 都可被伪造。RS256 和 ES256 使用非对称密钥:只有认证服务器持有私钥,其他服务只需公钥即可验证。

// Anatomy of a JWT — Header.Payload.Signature

// 1. Header (Base64URL decoded)
const header = {
  alg: "RS256",   // Algorithm: RS256 (asymmetric) preferred over HS256 (symmetric)
  typ: "JWT"
};

// 2. Payload (Base64URL decoded) — the claims
const payload = {
  iss: "https://auth.example.com",         // Issuer
  sub: "user_01HXYZ123",                  // Subject (user ID)
  aud: "https://api.example.com",          // Audience (target service)
  exp: 1735689600,                         // Expiry: Unix timestamp (15 min from now)
  iat: 1735688700,                         // Issued at
  jti: "8f14e0f9-21af-4c1e-8f26-3b4a5c6d7e8f", // Unique token ID (for revocation)
  roles: ["user"],                         // Custom claims
  email: "alice@example.com"
};

// 3. Signature (server-side only)
// RS256: sign(base64url(header) + '.' + base64url(payload), PRIVATE_KEY)
// Verification: verify(token, PUBLIC_KEY) — other services only need the public key

// --- Generating a JWT with jsonwebtoken (Node.js) ---
import jwt from 'jsonwebtoken';
import { readFileSync } from 'fs';
import { randomUUID } from 'crypto';

const privateKey = readFileSync('./private.pem');   // RSA private key
const publicKey  = readFileSync('./public.pem');    // RSA public key (shared with all services)

function issueAccessToken(userId: string, roles: string[]): string {
  return jwt.sign(
    {
      sub: userId,
      aud: 'https://api.example.com',
      roles,
      jti: randomUUID(),
    },
    privateKey,
    {
      algorithm: 'RS256',
      issuer: 'https://auth.example.com',
      expiresIn: '15m',  // Short-lived access token
    }
  );
}

function verifyAccessToken(token: string) {
  return jwt.verify(token, publicKey, {
    algorithms: ['RS256'],           // CRITICAL: whitelist algorithms — never allow 'none'
    issuer: 'https://auth.example.com',
    audience: 'https://api.example.com',
  });
}

Token 过期与轮换策略

短期 Access Token(15 分钟)配合长期 Refresh Token(7-30 天)是行业标准。Access Token 过期后,客户端静默地用 Refresh Token 换取新的令牌对。Refresh Token 必须存储在服务端(数据库中)以便可以撤销——不像 Access Token 是无状态的。

// Refresh Token Pattern — Stateless Access + Stateful Refresh

import jwt from 'jsonwebtoken';
import { randomBytes } from 'crypto';

// ---- Token issuance ----
async function issueTokenPair(userId: string) {
  const accessToken = jwt.sign(
    { sub: userId, jti: randomUUID() },
    process.env.JWT_PRIVATE_KEY!,
    { algorithm: 'RS256', expiresIn: '15m' }
  );

  // Refresh token: random bytes stored in DB — NOT a JWT
  const refreshToken = randomBytes(64).toString('hex');
  const refreshExpiry = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days

  // Store hashed refresh token in DB (never store plaintext)
  await db.refreshTokens.create({
    tokenHash: sha256(refreshToken),
    userId,
    expiresAt: refreshExpiry,
    createdAt: new Date(),
  });

  return { accessToken, refreshToken, refreshExpiry };
}

// ---- Token refresh endpoint ----
app.post('/auth/refresh', async (req, res) => {
  const { refreshToken } = req.cookies; // httpOnly cookie
  if (!refreshToken) return res.status(401).json({ error: 'No refresh token' });

  const stored = await db.refreshTokens.findOne({
    tokenHash: sha256(refreshToken),
    expiresAt: { >: new Date() }, // Not expired
    revokedAt: null,                  // Not revoked
  });

  if (!stored) return res.status(401).json({ error: 'Invalid or expired refresh token' });

  // Rotate: invalidate old, issue new pair (refresh token rotation)
  await db.refreshTokens.update(
    { id: stored.id },
    { revokedAt: new Date() }         // Revoke old refresh token
  );

  const newPair = await issueTokenPair(stored.userId);

  // Set new refresh token as httpOnly cookie
  res.cookie('refreshToken', newPair.refreshToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    expires: newPair.refreshExpiry,
    path: '/auth/refresh',            // Scope cookie to refresh endpoint only
  });

  return res.json({ accessToken: newPair.accessToken });
});

// ---- Logout: revoke refresh token ----
app.post('/auth/logout', async (req, res) => {
  const { refreshToken } = req.cookies;
  if (refreshToken) {
    await db.refreshTokens.update(
      { tokenHash: sha256(refreshToken) },
      { revokedAt: new Date() }
    );
  }
  res.clearCookie('refreshToken', { path: '/auth/refresh' });
  return res.json({ success: true });
});

常见 JWT 攻击与防御

最危险的 JWT 漏洞:(1)算法混淆——攻击者将 alg 改为 "none" 以绕过签名验证;(2)RS256→HS256 混淆——攻击者欺骗期望 RS256 的服务器将公钥作为 HS256 密钥;(3)弱密钥——小于 256 位的 HS256 密钥可被暴力破解;(4)缺少 exp 声明验证——库默认可能不验证过期时间。

// JWT Attack Defenses — Code Patterns

// ---- ATTACK 1: Algorithm confusion ("none" bypass) ----
// VULNERABLE: never use verify() without specifying allowed algorithms
const VULNERABLE = jwt.verify(token, secret); // attacker can set alg: "none"

// SAFE: always whitelist algorithms
const SAFE = jwt.verify(token, publicKey, {
  algorithms: ['RS256'], // Never include 'none', 'HS256' if you're using RS256
});

// ---- ATTACK 2: RS256 > HS256 confusion ----
// Attacker changes alg from RS256 to HS256 and signs with the PUBLIC key as the secret
// Defense: always specify algorithm on verify — never infer from the token header
// VULNERABLE:
const header = JSON.parse(atob(token.split('.')[0]));
jwt.verify(token, publicKey, { algorithms: [header.alg] }); // attacker controls this!

// SAFE:
jwt.verify(token, publicKey, { algorithms: ['RS256'] }); // hardcoded

// ---- ATTACK 3: Weak HS256 secrets (brute-force) ----
// VULNERABLE: short or predictable secrets
const BAD_SECRET = 'secret';
const BAD_SECRET2 = 'mysupersecretkey';

// SAFE: 256-bit cryptographically random secret
import { randomBytes } from 'crypto';
const GOOD_SECRET = randomBytes(32).toString('hex'); // 256-bit
// Store in environment variable, rotate annually

// ---- ATTACK 4: Missing expiry validation ----
// Some older libraries skip exp check by default
// SAFE: always validate exp (jsonwebtoken does this by default)
try {
  const decoded = jwt.verify(token, publicKey, {
    algorithms: ['RS256'],
    clockTolerance: 30, // Allow 30 seconds clock skew max
    ignoreExpiration: false, // NEVER set this to true in production
  });
} catch (err) {
  if (err instanceof jwt.TokenExpiredError) {
    return res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' });
  }
  return res.status(401).json({ error: 'Invalid token' });
}

// ---- Token blacklist (for early revocation via jti) ----
import { createClient } from 'redis';
const redis = createClient();

async function revokeToken(jti: string, exp: number) {
  const ttl = exp - Math.floor(Date.now() / 1000); // seconds until natural expiry
  if (ttl > 0) {
    await redis.set(`jwt:revoked:${jti}`, '1', { EX: ttl });
  }
}

async function isTokenRevoked(jti: string): Promise<boolean> {
  return (await redis.get(`jwt:revoked:${jti}`)) !== null;
}

// Middleware: check blacklist
app.use(async (req, res, next) => {
  const decoded = jwt.verify(req.headers.authorization?.split(' ')[1] ?? '', publicKey, {
    algorithms: ['RS256'],
  });
  if (await isTokenRevoked((decoded as jwt.JwtPayload).jti ?? '')) {
    return res.status(401).json({ error: 'Token revoked' });
  }
  next();
});

安全的 Token 存储

Access Token:存储在内存中(JavaScript 变量)——不存在 localStorage(易受 XSS 攻击)。Refresh Token:存储在 httpOnly、Secure、SameSite=Strict cookie 中。这种组合防止 XSS 读取 Refresh Token,SameSite 阻止跨源 cookie 提交。

常见问题

我应该使用 JWT 还是不透明会话 Token?

传统服务端渲染应用使用不透明会话 Token(存储在数据库中的随机 ID)——更简单、可即时撤销、无算法攻击面。微服务或需要跨多个服务进行无状态验证的 API 使用 JWT。

如何在过期前撤销 JWT?

JWT 设计上是无状态的,即时撤销较难。解决方案:(1)短过期时间(15 分钟);(2)Token 黑名单——在 Redis 中存储已撤销的 JTI;(3)用户记录中的 Token 版本——撤销时递增版本字段,在 JWT 中包含版本,拒绝版本过期的 Token。

每个 JWT 应包含哪些声明?

必须:iss(签发者)、sub(主体/用户 ID)、aud(受众/目标服务)、exp(过期时间)、iat(签发时间)、jti(唯一 Token ID 用于撤销)。始终在接收服务上验证 iss 和 aud,防止跨服务的 Token 重用。

JWT 密钥和私钥应该多长?

HS256 密钥:最少 256 位(32 字节)的加密随机数据,使用 crypto.randomBytes(32)。RS256/RS512:最少 2048 位 RSA 密钥(长期密钥推荐 4096 位)。ES256:256 位椭圆曲线密钥(P-256)。永远不要使用密码或人类可读字符串作为密钥。

相关工具

𝕏 Twitterin LinkedIn
这篇文章有帮助吗?

保持更新

获取每周开发技巧和新工具通知。

无垃圾邮件,随时退订。

试试这些相关工具

JWTJWT Decoder→BBase64 Decoder{ }JSON Formatter

相关文章

JWT 认证:完整实现指南

从零实现 JWT 认证。Token 结构、访问令牌和刷新令牌、Node.js 实现、客户端管理、安全最佳实践和 Next.js 中间件。

HTTP请求头完整指南:请求、响应和安全头

HTTP头完整指南:请求头、响应头和安全头,包括CORS、缓存控制。

REST API 设计指南:2026 年最佳实践

设计健壮的 REST API 全面指南:资源命名、HTTP 方法、分页、过滤、错误处理、版本管理、身份验证、缓存和 OpenAPI 文档。

在线 JWT 解码器:解码、检查和调试 JSON Web Token(2026 指南)

使用免费的在线 JWT 解码工具即时检查 JWT 头部、载荷和声明。涵盖 JWT 结构、标准声明、JavaScript/Python/Go/Java 解码、签名算法、安全最佳实践和常见错误。