From c19a5eea087f708d6a533482280655fd63737896 Mon Sep 17 00:00:00 2001 From: figmar Date: Mon, 31 Aug 2026 02:52:09 +0800 Subject: [PATCH] fix(gateway): migrate jjwt to 0.12.6 and fix removed Spring WebClient FormDataSpec - CasdoorIdTokenVerifier: RSAKey does not exist in any jjwt release; build RSAPublicKey from JWKS n/e via RSAPublicKeySpec, use 0.12 parser API - JwtUtils / UserService: parserBuilder/parseClaimsJws -> parser()/parseSignedClaims - CasdoorClient: WebClient.RequestBodySpec.FormDataSpec removed in Spring 6.2, use BodyInserters.FormDataSpec --- backend/api-gateway/pom.xml | 2 +- .../gateway/domain/service/UserService.java | 6 +- .../infrastructure/casdoor/CasdoorClient.java | 2 +- .../casdoor/CasdoorIdTokenVerifier.java | 39 +++++--- backend/shared/security-common/pom.xml | 92 +++++++++---------- .../deepdata/common/security/JwtUtils.java | 22 ++--- 6 files changed, 86 insertions(+), 77 deletions(-) diff --git a/backend/api-gateway/pom.xml b/backend/api-gateway/pom.xml index 2a711df..daa78ad 100644 --- a/backend/api-gateway/pom.xml +++ b/backend/api-gateway/pom.xml @@ -20,7 +20,7 @@ 3.5.6 2025.0.0 - 0.11.5 + 0.12.6 diff --git a/backend/api-gateway/src/main/java/com/deepdata/gateway/domain/service/UserService.java b/backend/api-gateway/src/main/java/com/deepdata/gateway/domain/service/UserService.java index afe97af..7718b60 100644 --- a/backend/api-gateway/src/main/java/com/deepdata/gateway/domain/service/UserService.java +++ b/backend/api-gateway/src/main/java/com/deepdata/gateway/domain/service/UserService.java @@ -75,7 +75,7 @@ public class UserService { .claim("role", user.getRole()) .setIssuedAt(new Date(now)) .setExpiration(new Date(now + expirationSeconds * 1000)) - .signWith(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)), SignatureAlgorithm.HS256); + .signWith(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8))); if (user.getUserGroups() != null && !user.getUserGroups().isEmpty()) { builder.claim("groups", user.getUserGroups()); } @@ -84,8 +84,8 @@ public class UserService { public String validateToken(String token) { try { - Jws claimsJws = Jwts.parserBuilder().setSigningKey(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8))).build().parseClaimsJws(token); - return claimsJws.getBody().getSubject(); + Jws claimsJws = Jwts.parser().verifyWith(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8))).build().parseSignedClaims(token); + return claimsJws.getPayload().getSubject(); } catch (JwtException | IllegalArgumentException ex) { return null; } diff --git a/backend/api-gateway/src/main/java/com/deepdata/gateway/infrastructure/casdoor/CasdoorClient.java b/backend/api-gateway/src/main/java/com/deepdata/gateway/infrastructure/casdoor/CasdoorClient.java index 70df517..1b3f44a 100644 --- a/backend/api-gateway/src/main/java/com/deepdata/gateway/infrastructure/casdoor/CasdoorClient.java +++ b/backend/api-gateway/src/main/java/com/deepdata/gateway/infrastructure/casdoor/CasdoorClient.java @@ -104,7 +104,7 @@ public class CasdoorClient { * 非 2xx 解析错误 JSON 并抛出带 error_description 的异常。 */ private Mono postToken( - WebClient.RequestBodySpec.FormDataSpec form, String op) { + BodyInserters.FormDataSpec form, String op) { return webClient.post() .uri(props.getTokenUri()) .contentType(MediaType.APPLICATION_FORM_URLENCODED) diff --git a/backend/api-gateway/src/main/java/com/deepdata/gateway/infrastructure/casdoor/CasdoorIdTokenVerifier.java b/backend/api-gateway/src/main/java/com/deepdata/gateway/infrastructure/casdoor/CasdoorIdTokenVerifier.java index 8f2c0bf..1b98f73 100644 --- a/backend/api-gateway/src/main/java/com/deepdata/gateway/infrastructure/casdoor/CasdoorIdTokenVerifier.java +++ b/backend/api-gateway/src/main/java/com/deepdata/gateway/infrastructure/casdoor/CasdoorIdTokenVerifier.java @@ -4,7 +4,6 @@ import com.deepdata.gateway.infrastructure.casdoor.dto.JwksResponse; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.security.RSAKey; import lombok.extern.slf4j.Slf4j; import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.stereotype.Component; @@ -13,6 +12,11 @@ import reactor.core.publisher.Mono; import reactor.netty.http.client.HttpClient; import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPublicKeySpec; import java.util.ArrayList; import java.util.Base64; import java.util.HashMap; @@ -22,7 +26,7 @@ import java.util.Optional; /** * Casdoor id_token 验证器:拉取公开 JWKS(带 TTL 缓存),按 {@code kid} 选 RSA 公钥, - * 用 JJWT(0.11.5,与网关现有 JWT 栈一致)做 RS256 验签 + iss/aud/exp/nbf 校验。 + * 用 JJWT(0.12.x)做 RS256 验签 + iss/aud/exp/nbf 校验。 * *

实测(2026-08-30):JWKS 端点 {@code /.well-known/jwks.json} 无需鉴权,返回 *

{"keys":[{"use":"sig","kty":"RSA","kid":"cert-built-in","alg":"RS256","n":"...","e":"AQAB","x5c":["..."]}]}
@@ -40,10 +44,10 @@ public class CasdoorIdTokenVerifier { private final CasdoorProperties props; private final WebClient webClient; - /** JWKS 缓存(kid → RSAKey),证书轮转时靠 TTL / kid 未命中强制刷新 */ + /** JWKS 缓存(kid → RSAPublicKey),证书轮转时靠 TTL / kid 未命中强制刷新 */ private volatile CacheEntry cache; - private record CacheEntry(long fetchedAtMillis, Map keys) {} + private record CacheEntry(long fetchedAtMillis, Map keys) {} public CasdoorIdTokenVerifier(CasdoorProperties props) { this.props = props; @@ -62,7 +66,7 @@ public class CasdoorIdTokenVerifier { public Mono verify(String idToken) { return parseAndVerify(idToken, false) .flatMap(jws -> { - Claims body = jws.getBody(); + Claims body = jws.getPayload(); IdTokenClaims claims = new IdTokenClaims( body.getSubject(), body.get("name", String.class), @@ -123,26 +127,26 @@ public class CasdoorIdTokenVerifier { }); } - private Jws parseWithKid(String idToken, Map keys) { - String kid = Jwts.parserBuilder().build().readHeader(idToken).get("kid", String.class); - RSAKey key = keys.get(kid); + private Jws parseWithKid(String idToken, Map keys) { + String kid = Jwts.parser().build().parse(idToken).getHeader().get("kid", String.class); + RSAPublicKey key = keys.get(kid); if (key == null) { log.warn("casdoor jwks: kid [{}] not cached, size={}", kid, keys.size()); return null; } try { - return Jwts.parserBuilder() - .setSigningKey(key) + return Jwts.parser() + .verifyWith(key) .requireIssuer(props.getIssuer()) .requireAudience(props.getClientId()) .build() - .parseClaimsJws(idToken); + .parseSignedClaims(idToken); } catch (Exception e) { throw new CasdoorIdTokenException("id_token verification failed: " + e.getMessage(), e); } } - private Mono> loadKeys(boolean forced) { + private Mono> loadKeys(boolean forced) { CacheEntry current = cache; long ttlMillis = props.getJwksCacheSeconds() * 1000L; if (!forced && current != null @@ -154,7 +158,7 @@ public class CasdoorIdTokenVerifier { .retrieve() .bodyToMono(JwksResponse.class) .map(jwks -> { - Map keys = new HashMap<>(); + Map keys = new HashMap<>(); if (jwks.getKeys() != null) { for (JwksResponse.Jwk jwk : jwks.getKeys()) { if ("RSA".equals(jwk.getKty()) && jwk.getKid() != null @@ -170,10 +174,15 @@ public class CasdoorIdTokenVerifier { .doOnError(e -> log.error("casdoor jwks fetch failed: {}", e.getMessage(), e)); } - private static RSAKey toRsaKey(JwksResponse.Jwk jwk) { + private static RSAPublicKey toRsaKey(JwksResponse.Jwk jwk) { BigInteger n = new BigInteger(1, Base64.getUrlDecoder().decode(jwk.getN())); BigInteger e = new BigInteger(1, Base64.getUrlDecoder().decode(jwk.getE())); - return new RSAKey.Builder(n, e).build(); + try { + return (RSAPublicKey) KeyFactory.getInstance("RSA") + .generatePublic(new RSAPublicKeySpec(n, e)); + } catch (NoSuchAlgorithmException | InvalidKeySpecException ex) { + throw new CasdoorIdTokenException("invalid JWKS RSA key: " + ex.getMessage(), ex); + } } /** id_token 验证失败(签名/iss/aud/exp 任一不过,或 JWKS 不可用) */ diff --git a/backend/shared/security-common/pom.xml b/backend/shared/security-common/pom.xml index f6887e1..873a5b8 100644 --- a/backend/shared/security-common/pom.xml +++ b/backend/shared/security-common/pom.xml @@ -1,46 +1,46 @@ - - - 4.0.0 - - - com.deepdata - services - 1.0.0-SNAPSHOT - ../../services/pom.xml - - - security-common - Security Common - 安全通用组件 - - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.boot - spring-boot-starter-oauth2-resource-server - - - io.jsonwebtoken - jjwt-api - 0.11.5 - - - io.jsonwebtoken - jjwt-impl - 0.11.5 - runtime - - - io.jsonwebtoken - jjwt-jackson - 0.11.5 - runtime - - - + + + 4.0.0 + + + com.deepdata + services + 1.0.0-SNAPSHOT + ../../services/pom.xml + + + security-common + Security Common + 安全通用组件 + + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + io.jsonwebtoken + jjwt-api + 0.12.6 + + + io.jsonwebtoken + jjwt-impl + 0.12.6 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.12.6 + runtime + + + diff --git a/backend/shared/security-common/src/main/java/com/deepdata/common/security/JwtUtils.java b/backend/shared/security-common/src/main/java/com/deepdata/common/security/JwtUtils.java index f3268f3..651126b 100644 --- a/backend/shared/security-common/src/main/java/com/deepdata/common/security/JwtUtils.java +++ b/backend/shared/security-common/src/main/java/com/deepdata/common/security/JwtUtils.java @@ -37,11 +37,11 @@ public class JwtUtils { tokenClaims.put("sub", username); return Jwts.builder() - .setClaims(tokenClaims) - .setSubject(username) - .setIssuedAt(new Date()) - .setExpiration(new Date(System.currentTimeMillis() + expiration * 1000)) - .signWith(getSigningKey(), SignatureAlgorithm.HS512) + .claims(tokenClaims) + .subject(username) + .issuedAt(new Date()) + .expiration(new Date(System.currentTimeMillis() + expiration * 1000)) + .signWith(getSigningKey()) .compact(); } @@ -63,11 +63,11 @@ public class JwtUtils { * 从令牌中获取声明 */ public Claims getClaimsFromToken(String token) { - return Jwts.parserBuilder() - .setSigningKey(getSigningKey()) + return Jwts.parser() + .verifyWith(getSigningKey()) .build() - .parseClaimsJws(token) - .getBody(); + .parseSignedClaims(token) + .getPayload(); } /** @@ -99,8 +99,8 @@ public class JwtUtils { claims.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000)); return Jwts.builder() - .setClaims(claims) - .signWith(getSigningKey(), SignatureAlgorithm.HS512) + .claims(claims) + .signWith(getSigningKey()) .compact(); } }