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>
<spring-boot.version>3.5.6</spring-boot.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>
<dependencies>
@@ -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<Claims> claimsJws = Jwts.parserBuilder().setSigningKey(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8))).build().parseClaimsJws(token);
return claimsJws.getBody().getSubject();
Jws<Claims> claimsJws = Jwts.parser().verifyWith(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8))).build().parseSignedClaims(token);
return claimsJws.getPayload().getSubject();
} catch (JwtException | IllegalArgumentException ex) {
return null;
}
@@ -104,7 +104,7 @@ public class CasdoorClient {
* 非 2xx 解析错误 JSON 并抛出带 error_description 的异常。
*/
private Mono<CasdoorTokenResponse> postToken(
WebClient.RequestBodySpec.FormDataSpec form, String op) {
BodyInserters.FormDataSpec form, String op) {
return webClient.post()
.uri(props.getTokenUri())
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
@@ -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 校验。
*
* <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>
@@ -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<String, RSAKey> keys) {}
private record CacheEntry(long fetchedAtMillis, Map<String, RSAPublicKey> keys) {}
public CasdoorIdTokenVerifier(CasdoorProperties props) {
this.props = props;
@@ -62,7 +66,7 @@ public class CasdoorIdTokenVerifier {
public Mono<IdTokenClaims> 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<Claims> parseWithKid(String idToken, Map<String, RSAKey> keys) {
String kid = Jwts.parserBuilder().build().readHeader(idToken).get("kid", String.class);
RSAKey key = keys.get(kid);
private Jws<Claims> parseWithKid(String idToken, Map<String, RSAPublicKey> 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<Map<String, RSAKey>> loadKeys(boolean forced) {
private Mono<Map<String, RSAPublicKey>> 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<String, RSAKey> keys = new HashMap<>();
Map<String, RSAPublicKey> 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 不可用) */
+46 -46
View File
@@ -1,46 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.deepdata</groupId>
<artifactId>services</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../../services/pom.xml</relativePath>
</parent>
<artifactId>security-common</artifactId>
<name>Security Common</name>
<description>安全通用组件</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.deepdata</groupId>
<artifactId>services</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../../services/pom.xml</relativePath>
</parent>
<artifactId>security-common</artifactId>
<name>Security Common</name>
<description>安全通用组件</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
@@ -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();
}
}