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>
|
<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>
|
||||||
|
|||||||
+3
-3
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -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)
|
||||||
|
|||||||
+24
-15
@@ -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 不可用) */
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
+11
-11
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user