From b6ea3fab0e01128859e23dbfab39c40003ec4f5a Mon Sep 17 00:00:00 2001 From: yofchio <2946476452@qq.com> Date: Sun, 15 Mar 2026 21:46:21 -0700 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20JWT=20=E7=AE=97?= =?UTF-8?q?=E6=B3=95=E9=99=8D=E7=BA=A7(alg:none)=E7=BB=95=E8=BF=87?= =?UTF-8?q?=E9=89=B4=E6=9D=83=E5=8F=8A=E5=A4=9A=E5=A4=84=20JWT=20=E5=AE=89?= =?UTF-8?q?=E5=85=A8=E6=BC=8F=E6=B4=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复了以下安全漏洞: 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 --- .../youlai/boot/config/WebSocketConfig.java | 8 +++++- .../boot/security/token/JwtTokenManager.java | 28 +++++++++++++++++++ src/main/resources/application-dev.yml | 2 +- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/youlai/boot/config/WebSocketConfig.java b/src/main/java/com/youlai/boot/config/WebSocketConfig.java index 1a0e5b2d..6a785a2d 100644 --- a/src/main/java/com/youlai/boot/config/WebSocketConfig.java +++ b/src/main/java/com/youlai/boot/config/WebSocketConfig.java @@ -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); diff --git a/src/main/java/com/youlai/boot/security/token/JwtTokenManager.java b/src/main/java/com/youlai/boot/security/token/JwtTokenManager.java index c3dd612c..c1c0715a 100644 --- a/src/main/java/com/youlai/boot/security/token/JwtTokenManager.java +++ b/src/main/java/com/youlai/boot/security/token/JwtTokenManager.java @@ -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 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()); + } + } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 21125ff4..ab14d330 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -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等)