fix(gateway): migrate jjwt to 0.12.6 and fix removed Spring WebClient FormDataSpec
CodeQL Advanced / Analyze (actions) (push) Canceled after 0s
CodeQL Advanced / Analyze (java-kotlin) (push) Canceled after 0s
CodeQL Advanced / Analyze (javascript-typescript) (push) Canceled after 0s
CodeQL Advanced / Analyze (python) (push) Canceled after 0s
Docker Image Build & Push / manifest (push) Canceled after 0s
Docker Image Build & Push / build-and-push-arm (push) Canceled after 0s
Docker Image Build & Push / build-and-push-amd (push) Canceled after 0s
Docker Image Build & Push / manifest (push) Canceled after 0s
Docker Image Build & Push / build-and-push-arm (push) Canceled after 0s
Docker Image Build & Push / build-and-push-amd (push) Canceled after 0s
Backend Docker Image CI / Build and Push Backend Docker Image (push) Canceled after 0s
Gateway Docker Image CI / Build and Push Gateway Docker Image (push) Canceled after 0s

- 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
This commit is contained in:
figmar
2026-08-31 02:52:09 +08:00
parent b94dae79fe
commit c19a5eea08
6 changed files with 86 additions and 77 deletions
+1 -1
View File
@@ -20,7 +20,7 @@
<properties> <properties>
<spring-boot.version>3.5.6</spring-boot.version> <spring-boot.version>3.5.6</spring-boot.version>
<spring-cloud.version>2025.0.0</spring-cloud.version> <spring-cloud.version>2025.0.0</spring-cloud.version>
<jjwt.version>0.11.5</jjwt.version> <jjwt.version>0.12.6</jjwt.version>
</properties> </properties>
<dependencies> <dependencies>
@@ -75,7 +75,7 @@ public class UserService {
.claim("role", user.getRole()) .claim("role", user.getRole())
.setIssuedAt(new Date(now)) .setIssuedAt(new Date(now))
.setExpiration(new Date(now + expirationSeconds * 1000)) .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()) { if (user.getUserGroups() != null && !user.getUserGroups().isEmpty()) {
builder.claim("groups", user.getUserGroups()); builder.claim("groups", user.getUserGroups());
} }
@@ -84,8 +84,8 @@ public class UserService {
public String validateToken(String token) { public String validateToken(String token) {
try { try {
Jws<Claims> claimsJws = Jwts.parserBuilder().setSigningKey(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8))).build().parseClaimsJws(token); Jws<Claims> claimsJws = Jwts.parser().verifyWith(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8))).build().parseSignedClaims(token);
return claimsJws.getBody().getSubject(); return claimsJws.getPayload().getSubject();
} catch (JwtException | IllegalArgumentException ex) { } catch (JwtException | IllegalArgumentException ex) {
return null; return null;
} }
@@ -104,7 +104,7 @@ public class CasdoorClient {
* 非 2xx 解析错误 JSON 并抛出带 error_description 的异常。 * 非 2xx 解析错误 JSON 并抛出带 error_description 的异常。
*/ */
private Mono<CasdoorTokenResponse> postToken( private Mono<CasdoorTokenResponse> postToken(
WebClient.RequestBodySpec.FormDataSpec form, String op) { BodyInserters.FormDataSpec form, String op) {
return webClient.post() return webClient.post()
.uri(props.getTokenUri()) .uri(props.getTokenUri())
.contentType(MediaType.APPLICATION_FORM_URLENCODED) .contentType(MediaType.APPLICATION_FORM_URLENCODED)
@@ -4,7 +4,6 @@ import com.deepdata.gateway.infrastructure.casdoor.dto.JwksResponse;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws; import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts; import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.RSAKey;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -13,6 +12,11 @@ import reactor.core.publisher.Mono;
import reactor.netty.http.client.HttpClient; import reactor.netty.http.client.HttpClient;
import java.math.BigInteger; 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.ArrayList;
import java.util.Base64; import java.util.Base64;
import java.util.HashMap; import java.util.HashMap;
@@ -22,7 +26,7 @@ import java.util.Optional;
/** /**
* Casdoor id_token 验证器:拉取公开 JWKS(带 TTL 缓存),按 {@code kid} 选 RSA 公钥, * 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 校验。
* *
* <p>实测(2026-08-30):JWKS 端点 {@code /.well-known/jwks.json} 无需鉴权,返回 * <p>实测(2026-08-30):JWKS 端点 {@code /.well-known/jwks.json} 无需鉴权,返回
* <pre>{"keys":[{"use":"sig","kty":"RSA","kid":"cert-built-in","alg":"RS256","n":"...","e":"AQAB","x5c":["..."]}]}</pre> * <pre>{"keys":[{"use":"sig","kty":"RSA","kid":"cert-built-in","alg":"RS256","n":"...","e":"AQAB","x5c":["..."]}]}</pre>
@@ -40,10 +44,10 @@ public class CasdoorIdTokenVerifier {
private final CasdoorProperties props; private final CasdoorProperties props;
private final WebClient webClient; private final WebClient webClient;
/** JWKS 缓存(kid → RSAKey),证书轮转时靠 TTL / kid 未命中强制刷新 */ /** JWKS 缓存(kid → RSAPublicKey),证书轮转时靠 TTL / kid 未命中强制刷新 */
private volatile CacheEntry cache; private volatile CacheEntry cache;
private record CacheEntry(long fetchedAtMillis, Map<String, RSAKey> keys) {} private record CacheEntry(long fetchedAtMillis, Map<String, RSAPublicKey> keys) {}
public CasdoorIdTokenVerifier(CasdoorProperties props) { public CasdoorIdTokenVerifier(CasdoorProperties props) {
this.props = props; this.props = props;
@@ -62,7 +66,7 @@ public class CasdoorIdTokenVerifier {
public Mono<IdTokenClaims> verify(String idToken) { public Mono<IdTokenClaims> verify(String idToken) {
return parseAndVerify(idToken, false) return parseAndVerify(idToken, false)
.flatMap(jws -> { .flatMap(jws -> {
Claims body = jws.getBody(); Claims body = jws.getPayload();
IdTokenClaims claims = new IdTokenClaims( IdTokenClaims claims = new IdTokenClaims(
body.getSubject(), body.getSubject(),
body.get("name", String.class), body.get("name", String.class),
@@ -123,26 +127,26 @@ public class CasdoorIdTokenVerifier {
}); });
} }
private Jws<Claims> parseWithKid(String idToken, Map<String, RSAKey> keys) { private Jws<Claims> parseWithKid(String idToken, Map<String, RSAPublicKey> keys) {
String kid = Jwts.parserBuilder().build().readHeader(idToken).get("kid", String.class); String kid = Jwts.parser().build().parse(idToken).getHeader().get("kid", String.class);
RSAKey key = keys.get(kid); RSAPublicKey key = keys.get(kid);
if (key == null) { if (key == null) {
log.warn("casdoor jwks: kid [{}] not cached, size={}", kid, keys.size()); log.warn("casdoor jwks: kid [{}] not cached, size={}", kid, keys.size());
return null; return null;
} }
try { try {
return Jwts.parserBuilder() return Jwts.parser()
.setSigningKey(key) .verifyWith(key)
.requireIssuer(props.getIssuer()) .requireIssuer(props.getIssuer())
.requireAudience(props.getClientId()) .requireAudience(props.getClientId())
.build() .build()
.parseClaimsJws(idToken); .parseSignedClaims(idToken);
} catch (Exception e) { } catch (Exception e) {
throw new CasdoorIdTokenException("id_token verification failed: " + e.getMessage(), e); throw new CasdoorIdTokenException("id_token verification failed: " + e.getMessage(), e);
} }
} }
private Mono<Map<String, RSAKey>> loadKeys(boolean forced) { private Mono<Map<String, RSAPublicKey>> loadKeys(boolean forced) {
CacheEntry current = cache; CacheEntry current = cache;
long ttlMillis = props.getJwksCacheSeconds() * 1000L; long ttlMillis = props.getJwksCacheSeconds() * 1000L;
if (!forced && current != null if (!forced && current != null
@@ -154,7 +158,7 @@ public class CasdoorIdTokenVerifier {
.retrieve() .retrieve()
.bodyToMono(JwksResponse.class) .bodyToMono(JwksResponse.class)
.map(jwks -> { .map(jwks -> {
Map<String, RSAKey> keys = new HashMap<>(); Map<String, RSAPublicKey> keys = new HashMap<>();
if (jwks.getKeys() != null) { if (jwks.getKeys() != null) {
for (JwksResponse.Jwk jwk : jwks.getKeys()) { for (JwksResponse.Jwk jwk : jwks.getKeys()) {
if ("RSA".equals(jwk.getKty()) && jwk.getKid() != null 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)); .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 n = new BigInteger(1, Base64.getUrlDecoder().decode(jwk.getN()));
BigInteger e = new BigInteger(1, Base64.getUrlDecoder().decode(jwk.getE())); 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 不可用) */ /** id_token 验证失败(签名/iss/aud/exp 任一不过,或 JWKS 不可用) */
+3 -3
View File
@@ -28,18 +28,18 @@
<dependency> <dependency>
<groupId>io.jsonwebtoken</groupId> <groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId> <artifactId>jjwt-api</artifactId>
<version>0.11.5</version> <version>0.12.6</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.jsonwebtoken</groupId> <groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId> <artifactId>jjwt-impl</artifactId>
<version>0.11.5</version> <version>0.12.6</version>
<scope>runtime</scope> <scope>runtime</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.jsonwebtoken</groupId> <groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version> <version>0.12.6</version>
<scope>runtime</scope> <scope>runtime</scope>
</dependency> </dependency>
</dependencies> </dependencies>
@@ -37,11 +37,11 @@ public class JwtUtils {
tokenClaims.put("sub", username); tokenClaims.put("sub", username);
return Jwts.builder() return Jwts.builder()
.setClaims(tokenClaims) .claims(tokenClaims)
.setSubject(username) .subject(username)
.setIssuedAt(new Date()) .issuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000)) .expiration(new Date(System.currentTimeMillis() + expiration * 1000))
.signWith(getSigningKey(), SignatureAlgorithm.HS512) .signWith(getSigningKey())
.compact(); .compact();
} }
@@ -63,11 +63,11 @@ public class JwtUtils {
* 从令牌中获取声明 * 从令牌中获取声明
*/ */
public Claims getClaimsFromToken(String token) { public Claims getClaimsFromToken(String token) {
return Jwts.parserBuilder() return Jwts.parser()
.setSigningKey(getSigningKey()) .verifyWith(getSigningKey())
.build() .build()
.parseClaimsJws(token) .parseSignedClaims(token)
.getBody(); .getPayload();
} }
/** /**
@@ -99,8 +99,8 @@ public class JwtUtils {
claims.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000)); claims.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000));
return Jwts.builder() return Jwts.builder()
.setClaims(claims) .claims(claims)
.signWith(getSigningKey(), SignatureAlgorithm.HS512) .signWith(getSigningKey())
.compact(); .compact();
} }
} }