fix: 修复 JWT 算法降级(alg:none)绕过鉴权及多处 JWT 安全漏洞

修复了以下安全漏洞:

1. JWT算法降级(alg:none)绕过签名校验 [严重]
   - validateToken 未校验 alg 字段,攻击者可将算法改为 none 绕过签名
   - 修复: 在 validateToken 中增加算法白名单校验,仅允许 HS256

2. parseToken 无安全校验,可伪造任意角色 [高危]
   - parseToken 直接信任 payload 中的 userId/authorities 等字段
   - 修复: 增加算法+签名双重校验(防御纵深)

3. WebSocket 连接不校验 Token 有效性 [严重]
   - handleConnect 只调用 parseToken,未调用 validateToken
   - 修复: 在 parseToken 前增加 validateToken 调用

4. invalidateToken 未验证 token 即操作 Redis [中危]
   - 直接解析未验证的 token 并用 jti 操作 Redis
   - 修复: 增加算法+签名校验后再执行撤销操作

Made-with: Cursor
This commit is contained in:
yofchio
2026-03-15 21:46:21 -07:00
parent 69f062d6b9
commit b6ea3fab0e
3 changed files with 36 additions and 2 deletions

View File

@@ -187,7 +187,13 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
throw new BadCredentialsException("Token 为空");
}
// 解析并验证 Token
// 校验 Token 有效性(算法+签名+过期时间+黑名单)
if (!tokenManager.validateToken(token)) {
log.warn("⚠ 非法连接请求Token 无效或已过期");
throw new BadCredentialsException("Token 无效或已过期");
}
// 解析 Token
Authentication authentication;
try {
authentication = tokenManager.parseToken(token);

View File

@@ -49,6 +49,8 @@ import java.util.stream.Collectors;
@Service
public class JwtTokenManager implements TokenManager {
private static final String ALLOWED_ALGORITHM = "HS256";
private final SecurityProperties securityProperties;
private final RedisTemplate<String, Object> redisTemplate;
private final byte[] secretKey;
@@ -91,6 +93,14 @@ public class JwtTokenManager implements TokenManager {
public Authentication parseToken(String token) {
JWT jwt = JWTUtil.parseToken(token);
if (!isAlgorithmAllowed(jwt)) {
throw new BusinessException(ResultCode.ACCESS_TOKEN_INVALID);
}
if (!jwt.setKey(secretKey).verify()) {
throw new BusinessException(ResultCode.ACCESS_TOKEN_INVALID);
}
JSONObject payloads = jwt.getPayloads();
SysUserDetails userDetails = new SysUserDetails();
userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID)); // 用户ID
@@ -164,6 +174,11 @@ public class JwtTokenManager implements TokenManager {
*/
private boolean validateToken(String token, boolean validateRefreshToken) {
JWT jwt = JWTUtil.parseToken(token);
if (!isAlgorithmAllowed(jwt)) {
return false;
}
// 检查 Token 是否有效(验签 + 是否过期)
boolean isValid = jwt.setKey(secretKey).validate(0);
@@ -219,6 +234,11 @@ public class JwtTokenManager implements TokenManager {
token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length());
}
JWT jwt = JWTUtil.parseToken(token);
if (!isAlgorithmAllowed(jwt) || !jwt.setKey(secretKey).verify()) {
return;
}
JSONObject payloads = jwt.getPayloads();
String jti = payloads.getStr(JWTPayload.JWT_ID);
Integer expirationAt = payloads.getInt(JWTPayload.EXPIRES_AT);
@@ -400,4 +420,12 @@ public class JwtTokenManager implements TokenManager {
return JWTUtil.createToken(payload, secretKey);
}
/**
* 校验JWT算法类型是否在允许的白名单中防止 alg:none 等算法降级攻击
*/
private boolean isAlgorithmAllowed(JWT jwt) {
Object algorithm = jwt.getHeader("alg");
return algorithm != null && ALLOWED_ALGORITHM.equals(algorithm.toString());
}
}

View File

@@ -87,7 +87,7 @@ security:
access-token-time-to-live: 7200 # 访问令牌 有效期(单位:秒),默认 2 小时,-1 表示永不过期
refresh-token-time-to-live: 604800 # 刷新令牌有效期(单位:秒),默认 7 天,-1 表示永不过期
jwt:
secret-key: SecretKey012345678901234567890123456789012345678901234567890123456789 # JWT密钥(HS256算法至少32字符)
secret-key: SecretKey012345678901234567890123456789012345678901234567890123456789 # JWT密钥(HS256算法至少32字符) 【安全警告】生产环境必须更换此默认密钥否则攻击者可直接伪造合法Token
redis-token:
allow-multi-login: true # 是否允许多设备登录
# 安全白名单路径,仅跳过 AuthorizationFilter 过滤器,还是会走 Spring Security 的其他过滤器(CSRF、CORS等)