java-security-issues

JWT 安全漏洞

最后更新:2026-04-17

概述

JSON Web Token(JWT)是广泛使用的无状态认证方案。但错误实现会导致签名绕过、权限提升、信息泄露等严重安全问题。

风险等级

维度 评级
OWASP Top 10 A07:2025 - Authentication Failures
CWE CWE-327 / CWE-326 / CWE-200
严重程度 高危

攻击类型

攻击方式 说明 危害
alg:none 攻击 将算法置为 none,去除签名 完全绕过签名验证
RS256 → HS256 混淆 将非对称算法改为对称,用公钥当密钥 伪造任意 Token
弱密钥暴力破解 密钥过短,离线爆破 伪造任意 Token
JWT 信息泄露 Payload 明文存储敏感信息 敏感数据泄露
未校验 exp/iss 过期或无效 Token 仍被接受 权限持续滥用

Java 场景

alg:none 攻击

// [VULNERABLE] 未限制允许的算法
@Component
public class JwtValidator {

    public Claims parse(String token) {
        // 危险:未指定允许的算法,攻击者可提交 alg:none 的 Token
        return Jwts.parserBuilder()
            .setSigningKey(secretKey)
            .build()
            .parseClaimsJws(token)
            .getBody();
    }
}
// [SECURE] 明确指定允许的算法
@Component
public class SecureJwtValidator {

    @Value("${jwt.secret}")
    private String secret;

    public Claims parse(String token) {
        return Jwts.parserBuilder()
            .setSigningKey(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)))
            // 安全:显式指定仅接受 HS256
            .requireAlgorithm("HS256")
            .build()
            .parseClaimsJws(token)
            .getBody();
    }
}

RS256 → HS256 算法混淆

// [VULNERABLE] 动态使用 Token Header 中声明的算法
public boolean verify(String token, PublicKey publicKey) {
    // 危险:攻击者将 Header 中 alg 改为 HS256,用公钥作为 HMAC 密钥
    String[] parts = token.split("\\.");
    String headerJson = new String(Base64.getDecoder().decode(parts[0]));
    String alg = parseAlg(headerJson); // 从 Token 中读取算法
    return verifyWithAlg(token, alg, publicKey); // 使用攻击者指定的算法
}
// [SECURE] 服务端固定算法,不信任 Token Header 中的算法声明
@Component
public class RsaJwtService {

    private final RSAPublicKey publicKey;

    public Claims verify(String token) {
        // 安全:固定使用 RS256,不读取 Token 中声明的算法
        Algorithm algorithm = Algorithm.RSA256(publicKey, null);
        JWTVerifier verifier = JWT.require(algorithm)
            .withIssuer("your-app")
            .build();
        DecodedJWT decoded = verifier.verify(token);
        return extractClaims(decoded);
    }
}

弱密钥与不安全配置

// [VULNERABLE] 密钥过短、硬编码、未校验过期时间
@Component
public class WeakJwtService {

    // 危险:密钥过短(< 256 bit),可被离线爆破
    private static final String SECRET = "secret";

    public String generate(String userId) {
        return Jwts.builder()
            .setSubject(userId)
            // 危险:未设置过期时间,Token 永不失效
            .signWith(Keys.hmacShaKeyFor(SECRET.getBytes()))
            .compact();
    }

    public Claims parse(String token) {
        return Jwts.parserBuilder()
            .setSigningKey(SECRET.getBytes())
            .build()
            .parseClaimsJws(token)
            // 危险:未校验 exp/iss/aud 等 Claims
            .getBody();
    }
}
// [SECURE] 强密钥、设置过期时间、完整校验 Claims
@Component
public class SecureJwtService {

    // 从配置中心或环境变量读取,长度 >= 256 bit
    @Value("${jwt.secret}")
    private String secret;

    private static final long EXPIRATION_MS = 3600_000L; // 1 小时

    public String generate(String userId, String role) {
        byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
        SecretKey key = Keys.hmacShaKeyFor(keyBytes);

        return Jwts.builder()
            .setSubject(userId)
            .claim("role", role)
            .setIssuer("your-app")
            .setAudience("your-client")
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_MS))
            .signWith(key, SignatureAlgorithm.HS256)
            .compact();
    }

    public Claims parse(String token) {
        byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
        return Jwts.parserBuilder()
            .setSigningKey(keyBytes)
            .requireIssuer("your-app")    // 校验签发者
            .requireAudience("your-client") // 校验受众
            .build()
            .parseClaimsJws(token)        // 自动校验 exp
            .getBody();
    }
}

JWT Payload 敏感信息泄露

// [VULNERABLE] Payload 中存储敏感信息
public String generateToken(User user) {
    return Jwts.builder()
        .setSubject(user.getId())
        // 危险:Payload 仅 Base64 编码,任何人都可以解码
        .claim("password", user.getPassword())
        .claim("creditCard", user.getCreditCard())
        .signWith(secretKey)
        .compact();
}
// [SECURE] Payload 只存储必要的非敏感标识
public String generateToken(User user) {
    return Jwts.builder()
        .setSubject(user.getId().toString())
        // 安全:只存放角色/权限等非敏感元数据
        .claim("roles", user.getRoles())
        .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_MS))
        .signWith(secretKey, SignatureAlgorithm.HS256)
        .compact();
}

检测方法

  1. 静态分析:检查 SECRET 是否硬编码、是否设置 setExpiration
  2. 手动测试:将 Header 中 alg 改为 none 并去掉签名,观察是否通过
  3. 工具:jwt_tool、hashcat 对弱密钥进行字典攻击

防护措施

  1. 固定算法:服务端不接受 Token 中声明的算法,固定使用 HS256 或 RS256
  2. 密钥强度:HS256 密钥 >= 256 bit,来自安全随机数,不硬编码
  3. 必须校验exp(过期)、iss(签发者)、aud(受众)
  4. Token 轮换:设置短有效期(15 分钟~1 小时),配合 Refresh Token
  5. Payload 最小化:不存储密码、手机号、身份证等敏感信息
  6. Token 吊销:维护黑名单或使用短有效期 + Refresh Token

参考资料