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
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:
@@ -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>
|
||||
|
||||
+3
-3
@@ -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;
|
||||
}
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
+24
-15
@@ -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 不可用) */
|
||||
|
||||
@@ -28,18 +28,18 @@
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>0.11.5</version>
|
||||
<version>0.12.6</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>0.11.5</version>
|
||||
<version>0.12.6</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>0.11.5</version>
|
||||
<version>0.12.6</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
+11
-11
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user