JWT 安全攻防实战:从算法混淆到 Token 泄露的完整防御指南

深入剖析 JWT 常见安全漏洞,包括算法混淆攻击、密钥泄露、Token 重放等真实攻击手法,附 Node.js 和 Java 完整攻防代码,帮你构建生产级 JWT 安全体系。

安全与密码 2026-06-02 18 分钟

JWT(JSON Web Token)是现代 Web 应用中最广泛使用的认证机制之一。根据 Auth0 2025 年的统计,超过 85% 的 REST API 使用 JWT 作为认证令牌,但其中约 40% 存在至少一个安全配置缺陷。JWT 的设计初衷是简洁高效,但正是这种「看起来很简单」的特性,让无数开发者在安全细节上栽了跟头——从 2015 年的 algorithm confusion 攻击到 2024 年的大规模 JWT 密钥泄露事件,每一次都在提醒我们:JWT 的安全性不在于它本身,而在于你如何使用它

🔐 一、JWT 基础回顾与安全模型

1.1 JWT 结构与签名机制

JWT 由三部分组成:Header(头部)、Payload(载荷)、Signature(签名),以 . 分隔。签名是 JWT 安全性的核心——它确保 Token 没有被篡改。

// JWT 的三部分结构示例
// Header: {"alg": "HS256", "typ": "JWT"}
// Payload: {"sub": "user123", "role": "admin", "exp": 1717401600}
// Signature: HMACSHA256(base64(header) + "." + base64(payload), secret)

// 使用 jose 库创建和验证 JWT
import * as jose from 'jose';

// 创建签名密钥
const secret = new TextEncoder().encode('your-256-bit-secret-minimum-length!');

// 生成 JWT
const token = await new jose.SignJWT({ sub: 'user123', role: 'admin' })
  .setProtectedHeader({ alg: 'HS256' })
  .setIssuedAt()
  .setExpirationTime('2h')
  .setIssuer('https://api.example.com')
  .sign(secret);

console.log(token);
// eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyMTIzIiwicm9sZSI6ImFkbWluIiwiaWF0Ijox...

1.2 JWT 安全的核心矛盾

JWT 的安全模型基于一个关键假设:签名密钥只有服务器知道。但实际生产环境中,这个假设经常被打破:

  • ❌ 密钥硬编码在前端代码或 Docker 镜像中
  • ❌ 使用弱密钥(如 123456password
  • ❌ 不验证 alg 字段,允许攻击者指定算法
  • ❌ 不设置过期时间,Token 永久有效

📌 记住: JWT 本身不是安全机制,它只是一个签名验证格式。安全与否完全取决于你的实现方式。一个配置错误的 JWT 比 Session Cookie 更危险,因为 JWT 中的信息对任何人都是可读的。

⚔️ 二、JWT 常见攻击手法与防御

2.1 🔴 算法混淆攻击(Algorithm Confusion)

这是 JWT 最经典的攻击手法,由 Auth0 安全团队在 2015 年首次公开。攻击原理:当服务器使用 RSA 公钥验证 JWT 时,攻击者将 Header 中的 algRS256 改为 HS256,然后用 RSA 公钥作为 HMAC 密钥签名——由于 RSA 公钥是公开的,攻击者可以伪造任意 Token。

// ⚠️ 攻击演示:算法混淆攻击
// 假设服务器使用 RS256(RSA 非对称签名),公钥已公开

import * as jose from 'jose';
import crypto from 'crypto';

// 服务器的 RSA 公钥(通常可以从 JWKS 端点获取)
const publicKeyPem = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Z3VS5JJcds3xfn/ygWe
...
-----END PUBLIC KEY-----`;

// ❌ 错误的验证方式:不检查 alg 字段
async function vulnerableVerify(token, publicKey) {
  // 解码 header 但不验证 alg
  const { payload } = await jose.jwtVerify(token, publicKey);
  return payload; // 攻击者可以用 HS256 + 公钥签名!
}

// ✅ 正确的防御方式:强制指定允许的算法
async function secureVerify(token, publicKey) {
  const { payload } = await jose.jwtVerify(token, publicKey, {
    algorithms: ['RS256'], // 明确指定只接受 RS256
  });
  return payload;
}

⚠️ 警告: 如果你的 JWT 验证代码没有明确指定 algorithms 白名单,你的系统就存在算法混淆漏洞。这是一个 2015 年就被发现的漏洞,但直到 2026 年仍然在生产系统中频繁出现。

2.2 🔑 弱密钥与密钥泄露攻击

根据 GitGuardian 2025 年报告,每年有超过 1200 万个 JWT 密钥被泄露到公开代码仓库中。更常见的问题是使用弱密钥——攻击者可以在几秒内暴力破解。

# 使用 hashcat 暴力破解弱 JWT 密钥
# HS256 用 common passwords 字典破解,通常几秒到几分钟
hashcat -a 0 -m 16500 jwt_token.txt common_passwords.txt

# 使用 john the ripper
john --wordlist=rockyou.txt --format=HMAC-SHA256 jwt_token.txt

# 实测数据:不同密钥强度的破解时间(RTX 4090)
# "secret"        → 0.1 秒
# "my-secret-key" → 3 秒
# "P@ssw0rd123!"  → 45 秒
# 32字节随机密钥   → 10^19 年(不可行)
// ✅ 正确的密钥生成方式
import crypto from 'crypto';

// 生成 256 位(32 字节)随机密钥
const secret = crypto.randomBytes(32);
console.log(secret.toString('base64'));
// 输出类似:K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols=

// ❌ 绝对不要这样做
const badSecrets = [
  'secret',           // 常见密码,秒破
  'my-jwt-secret',    // 弱密钥
  process.env.JWT_SECRET, // 如果 .env 文件泄露
  'hardcoded-in-code', // 代码仓库泄露
];

2.3 💀 Token 泄露与重放攻击

JWT 一旦签发,在过期之前都是有效的。如果 Token 被泄露(通过 XSS、日志泄露、中间人攻击等),攻击者可以在有效期内冒充用户。

// ✅ 防止 Token 重放攻击:绑定请求上下文
import * as jose from 'jose';

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

// 生成时绑定 IP 和 User-Agent 的指纹
async function createSecureToken(userId, req) {
  const fingerprint = crypto
    .createHash('sha256')
    .update(`${req.ip}-${req.headers['user-agent']}`)
    .digest('hex')
    .slice(0, 16); // 取前 16 字符作为指纹

  return new jose.SignJWT({
    sub: userId,
    fp: fingerprint, // 绑定指纹
    jti: crypto.randomUUID(), // 唯一 ID,防止重放
  })
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt()
    .setExpirationTime('15m') // 短过期时间
    .setIssuer('https://api.example.com')
    .sign(secret);
}

// 验证时检查指纹
async function verifySecureToken(token, req) {
  const { payload } = await jose.jwtVerify(token, secret, {
    algorithms: ['HS256'],
    issuer: 'https://api.example.com',
  });

  // 验证请求指纹
  const currentFp = crypto
    .createHash('sha256')
    .update(`${req.ip}-${req.headers['user-agent']}`)
    .digest('hex')
    .slice(0, 16);

  if (payload.fp !== currentFp) {
    throw new Error('Token fingerprint mismatch — possible theft');
  }

  return payload;
}

⚠️ 警告: JWT 不支持「撤销」。一旦签发,在过期前都有效。如果你需要撤销能力,必须引入 Token 黑名单(如 Redis)或使用极短过期时间 + Refresh Token 机制。

🛡️ 三、JWT 生产环境安全实践

3.1 📋 攻击手法与防御速查表

攻击手法 危害等级 攻击原理 防御措施
算法混淆 🔴 严重 将 RS256 改为 HS256,用公钥签名 强制指定 algorithms 白名单
弱密钥破解 🔴 严重 暴力破解弱 HMAC 密钥 使用 ≥256 位随机密钥
none 算法 🔴 严重 将 alg 设为 none,跳过签名验证 拒绝接受 alg: none
密钥泄露 🟠 高危 密钥出现在代码/日志/环境变量中 使用密钥管理服务(AWS KMS、HashiCorp Vault)
Token 重放 🟠 高危 窃取 Token 后重复使用 绑定 IP/指纹 + 短过期 + jti 去重
Payload 篡改 🟡 中危 解码后修改 payload 再编码 始终验证签名,不要只 base64 解码
Kid 注入 🟡 中危 修改 kid header 指向恶意文件 验证 kid 值,使用白名单

3.2 🔐 Java Spring Security 中的 JWT 安全实现

// ✅ Spring Security 中安全的 JWT 验证配置
import org.springframework.security.oauth2.jwt.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class JwtSecurityConfig {

    @Bean
    public JwtDecoder jwtDecoder() {
        // 使用 Nimbus 库,自动从 JWKS 端点获取公钥
        NimbusJwtDecoder decoder = NimbusJwtDecoder
            .withJwkSetUri("https://auth.example.com/.well-known/jwks.json")
            .build();

        // 关键:设置 JWT 验证器,强制检查所有声明
        decoder.setJwtValidator(
            JwtValidators.createDefaultWithIssuer("https://auth.example.com")
        );

        return decoder;
    }

    @Bean
    public JwtEncoder jwtEncoder() {
        // 使用 RSA 2048 位密钥对
        RSAKey rsaKey = generateRSAKeyPair(); // 从密钥管理服务加载
        JWKSource<SecurityContext> jwkSource = new ImmutableJWKSet<>(new JWKSet(rsaKey));
        return new NimbusJwtEncoder(jwkSource);
    }

    // 生成 JWT 时的安全配置
    public String generateToken(Authentication auth) {
        Instant now = Instant.now();
        JwtClaimsSet claims = JwtClaimsSet.builder()
            .issuer("https://auth.example.com")
            .issuedAt(now)
            .expiresAt(now.plus(Duration.ofMinutes(15))) // 短过期
            .subject(auth.getName())
            .claim("scope", auth.getAuthorities()) // 最小权限
            .claim("jti", UUID.randomUUID().toString()) // 唯一 ID
            .build();

        JwsHeader header = JwsHeader.with(SignatureAlgorithm.RS256)
            .keyId("current-key-id") // 指定密钥 ID
            .build();

        return jwtEncoder.encode(JwtEncoderParameters.from(header, claims)).getTokenValue();
    }
}

3.3 🔄 Access Token + Refresh Token 双 Token 方案

单个 JWT 的最大问题是无法撤销。生产环境的标准方案是使用双 Token 机制:

// ✅ 完整的双 Token 认证流程
import * as jose from 'jose';
import crypto from 'crypto';

const accessSecret = new TextEncoder().encode(process.env.ACCESS_TOKEN_SECRET);
const refreshSecret = new TextEncoder().encode(process.env.REFRESH_TOKEN_SECRET);

class TokenService {
  // 生成 Token 对
  async generateTokenPair(userId, req) {
    const jti = crypto.randomUUID();

    // Access Token:短生命周期(15 分钟),携带用户信息
    const accessToken = await new jose.SignJWT({
      sub: userId,
      type: 'access',
      jti,
    })
      .setProtectedHeader({ alg: 'HS256' })
      .setIssuedAt()
      .setExpirationTime('15m')
      .setIssuer('https://api.example.com')
      .sign(accessSecret);

    // Refresh Token:长生命周期(7 天),仅用于刷新
    const refreshToken = await new jose.SignJWT({
      sub: userId,
      type: 'refresh',
      jti, // 关联同一 jti,实现 Token 家族追踪
    })
      .setProtectedHeader({ alg: 'HS256' })
      .setIssuedAt()
      .setExpirationTime('7d')
      .setIssuer('https://api.example.com')
      .sign(refreshSecret);

    // 将 refresh token 的 jti 存入 Redis,支持撤销
    await redis.set(`refresh:${jti}`, userId, 'EX', 7 * 24 * 3600);

    return { accessToken, refreshToken, expiresIn: 900 };
  }

  // 刷新 Token
  async refreshToken(refreshToken, req) {
    const { payload } = await jose.jwtVerify(refreshToken, refreshSecret, {
      algorithms: ['HS256'],
      issuer: 'https://api.example.com',
    });

    if (payload.type !== 'refresh') {
      throw new Error('Invalid token type');
    }

    // 检查 refresh token 是否被撤销
    const exists = await redis.get(`refresh:${payload.jti}`);
    if (!exists) {
      // Token 家族被盗!撤销该用户的所有 Token
      await this.revokeAllUserTokens(payload.sub);
      throw new Error('Token reuse detected — all sessions revoked');
    }

    // 撤销旧的 refresh token(Rotation)
    await redis.del(`refresh:${payload.jti}`);

    // 生成新的 Token 对
    return this.generateTokenPair(payload.sub, req);
  }

  // 撤销用户所有 Token
  async revokeAllUserTokens(userId) {
    const keys = await redis.keys(`refresh:*`);
    for (const key of keys) {
      const uid = await redis.get(key);
      if (uid === userId) {
        await redis.del(key);
      }
    }
  }
}

💡 提示: Access Token 存内存或短期 Cookie,Refresh Token 存 HttpOnly + Secure + SameSite=Strict 的 Cookie。绝不要将 Token 存在 localStorage——任何 XSS 漏洞都能直接读取。

📊 四、JWT vs Session 方案对比

选择 JWT 还是传统 Session 取决于你的架构需求。以下是关键维度的对比:

维度 JWT Session + Cookie
无状态性 ✅ 完全无状态 ❌ 需要服务端存储
跨服务认证 ✅ 天然支持 ⚠️ 需要共享 Session Store
撤销能力 ❌ 无法直接撤销 ✅ 随时删除 Session
安全性 ⚠️ 配置复杂,易出错 ✅ 框架内置保护
性能 ✅ 无服务端查询 ⚠️ 每次请求查 Redis/DB
移动端支持 ✅ 原生友好 ⚠️ Cookie 支持有限
适用场景 微服务、移动端、SPA 传统 Web 应用、高安全要求

关键结论: 如果你只有一个单体 Web 应用,Session + Cookie 通常更安全、更简单。JWT 的真正优势在于分布式系统和跨服务认证——只有在这种场景下,JWT 的复杂性才是值得的。

🎯 五、JWT 安全检查清单

在部署 JWT 认证系统之前,逐条检查以下项目:

签名与算法:

  • ✅ 使用 RS256 或 ES256(非对称算法),避免在多服务场景使用 HS256
  • ✅ 密钥长度 ≥ 2048 位(RSA)或 256 位(EC/HMAC)
  • ✅ 强制指定 algorithms 白名单,不依赖 Header 中的 alg
  • ✅ 密钥通过密钥管理服务(AWS KMS、HashiCorp Vault)管理

Token 设计:

  • ✅ 设置合理的过期时间:Access Token ≤ 15 分钟,Refresh Token ≤ 7 天
  • ✅ 包含 iss(签发者)和 aud(受众)声明
  • ✅ 包含 jti(唯一 ID)支持去重和撤销
  • ✅ Payload 中不存储敏感信息(密码、身份证号等)

传输与存储:

  • ✅ Access Token 通过 Authorization: Bearer 头传输
  • ✅ Refresh Token 存储在 HttpOnly + Secure + SameSite=Strict Cookie 中
  • ❌ 绝不将 Token 存储在 localStorage
  • ❌ 绝不在 URL 参数中传递 Token

验证逻辑:

  • ✅ 验证签名、过期时间、签发者、受众
  • ✅ 检查 Token 是否在黑名单中(如果需要撤销)
  • ✅ 对 Refresh Token 实现 Rotation(每次刷新后旧 Token 失效)

📝 总结

JWT 是一把双刃剑。用好了,它是分布式认证的利器;用不好,它是安全灾难的根源。核心原则只有三条:

  1. 最小信任:验证每一个字段,不依赖任何默认行为
  2. 最短生命周期:Access Token 15 分钟,Refresh Token 7 天,越短越安全
  3. 最严存储:HttpOnly Cookie + HTTPS,绝不暴露给 JavaScript

推荐工具与库:

  • 🔧 jose — 现代 JavaScript JWT 库,支持所有算法
  • 🔧 Nimbus JOSE — Java 生态最成熟的 JWT 库
  • 🔧 jwt.io — 在线调试 JWT(仅用于开发,不要输入生产密钥)
  • 🔧 OWASP JWT Cheat Sheet — OWASP 官方安全指南
  • 🔧 jwt_tool — JWT 安全测试工具

📚 相关文章