优化
This commit is contained in:
@@ -9,7 +9,7 @@ import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtValidators;
|
||||
@@ -17,8 +17,9 @@ import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
import org.springframework.web.cors.reactive.CorsConfigurationSource;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimValidator;
|
||||
import java.util.List;
|
||||
import java.time.Instant;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
@@ -44,6 +45,9 @@ public class GatewaySecurityConfig {
|
||||
.access(new AppScopeAuthorizationManager("MERCHANT"));
|
||||
exchanges.pathMatchers("/api/platform/**")
|
||||
.access(new AppScopeAuthorizationManager("PLATFORM"));
|
||||
exchanges.pathMatchers("/api/pda/**")
|
||||
.access(new AppScopeAuthorizationManager("PLATFORM"));
|
||||
exchanges.pathMatchers("/internal/**").denyAll();
|
||||
exchanges.anyExchange().authenticated();
|
||||
return http.cors(cors -> cors.configurationSource(corsConfigurationSource))
|
||||
.csrf(ServerHttpSecurity.CsrfSpec::disable)
|
||||
@@ -59,12 +63,11 @@ public class GatewaySecurityConfig {
|
||||
|
||||
@Bean
|
||||
ReactiveJwtDecoder reactiveJwtDecoder(GatewaySecurityProperties properties) {
|
||||
byte[] key = properties.jwtSecret().getBytes(StandardCharsets.UTF_8);
|
||||
if (key.length < 32) throw new IllegalStateException("JWT_SECRET 至少需要 32 字节");
|
||||
NimbusReactiveJwtDecoder decoder = NimbusReactiveJwtDecoder
|
||||
.withSecretKey(new SecretKeySpec(key, "HmacSHA256"))
|
||||
.macAlgorithm(MacAlgorithm.HS256).build();
|
||||
decoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(properties.issuer()));
|
||||
.withJwkSetUri(properties.jwkSetUri()).jwsAlgorithm(SignatureAlgorithm.RS256).build();
|
||||
decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(
|
||||
JwtValidators.createDefaultWithIssuer(properties.issuer()),
|
||||
new JwtClaimValidator<List<String>>("aud", values -> values != null && values.contains(properties.audience()))));
|
||||
return decoder;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,11 @@ import java.util.List;
|
||||
* 网关安全配置模型,集中承载 JWT 密钥、签发方、Redis 令牌键前缀和匿名放行路径。
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "baiyue.security")
|
||||
public record GatewaySecurityProperties(String jwtSecret, String issuer, String tokenKeyPrefix,
|
||||
public record GatewaySecurityProperties(String jwkSetUri, String issuer, String audience, String tokenKeyPrefix,
|
||||
List<String> permitPaths) {
|
||||
public GatewaySecurityProperties {
|
||||
issuer = issuer == null || issuer.isBlank() ? "auth-service" : issuer;
|
||||
audience = audience == null || audience.isBlank() ? "lastleg-api" : audience;
|
||||
tokenKeyPrefix = normalize(tokenKeyPrefix);
|
||||
permitPaths = permitPaths == null ? List.of() : List.copyOf(permitPaths);
|
||||
}
|
||||
|
||||
@@ -42,6 +42,14 @@ spring:
|
||||
enabled: false
|
||||
lower-case-service-id: true
|
||||
routes:
|
||||
- id: lastleg-service
|
||||
uri: lb://lastleg-service
|
||||
predicates:
|
||||
- Path=/api/merchant/v1/last-leg/**,/api/platform/v1/last-leg/**,/api/pda/v1/last-leg/**
|
||||
- id: warehouse-overseas-service
|
||||
uri: lb://warehouse-overseas-service
|
||||
predicates:
|
||||
- Path=/api/platform/v1/overseas-warehouse-resources/**
|
||||
- id: auth-service
|
||||
uri: lb://auth-service
|
||||
predicates:
|
||||
@@ -83,6 +91,18 @@ spring:
|
||||
predicates:
|
||||
- Path=/api/platform/finance/v1/**,/api/client/finance/v1/**
|
||||
# Swagger 文档聚合路由。
|
||||
- id: lastleg-openapi
|
||||
uri: lb://lastleg-service
|
||||
predicates:
|
||||
- Path=/lastleg-service/v3/api-docs/**
|
||||
filters:
|
||||
- StripPrefix=1
|
||||
- id: warehouse-overseas-openapi
|
||||
uri: lb://warehouse-overseas-service
|
||||
predicates:
|
||||
- Path=/warehouse-overseas-service/v3/api-docs/**
|
||||
filters:
|
||||
- StripPrefix=1
|
||||
- id: auth-openapi
|
||||
uri: lb://auth-service
|
||||
predicates:
|
||||
@@ -158,7 +178,9 @@ baiyue:
|
||||
allow-credentials: ${CORS_ALLOW_CREDENTIALS:false}
|
||||
max-age: ${CORS_MAX_AGE:3600}
|
||||
security:
|
||||
jwt-secret: ${JWT_SECRET:change-me-to-at-least-32-bytes-long-before-start}
|
||||
jwk-set-uri: ${JWT_JWKS_URI:http://auth-service:9100/api/auth/.well-known/jwks.json}
|
||||
issuer: ${JWT_ISSUER:auth-service}
|
||||
audience: ${JWT_AUDIENCE:lastleg-api}
|
||||
token-key-prefix: ${REDIS_TOKEN_KEY_PREFIX:baiyue:auth}
|
||||
permit-paths:
|
||||
- /api/pda-updates/**
|
||||
@@ -183,6 +205,8 @@ baiyue:
|
||||
- /integration-service/v3/api-docs/**
|
||||
- /event-bridge-service/v3/api-docs/**
|
||||
- /finance-service/v3/api-docs/**
|
||||
- /lastleg-service/v3/api-docs/**
|
||||
- /warehouse-overseas-service/v3/api-docs/**
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
@@ -231,3 +255,7 @@ springdoc:
|
||||
url: /event-bridge-service/v3/api-docs
|
||||
- name: finance
|
||||
url: /finance-service/v3/api-docs
|
||||
- name: lastleg
|
||||
url: /lastleg-service/v3/api-docs
|
||||
- name: warehouse-overseas
|
||||
url: /warehouse-overseas-service/v3/api-docs
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
"spring.cloud.nacos.discovery.register-enabled=false",
|
||||
"spring.cloud.nacos.config.enabled=false",
|
||||
"spring.cloud.gateway.discovery.locator.enabled=false",
|
||||
"baiyue.security.jwt-secret=test-jwt-secret-at-least-32-bytes-long"
|
||||
"baiyue.security.jwk-set-uri=http://127.0.0.1:1/jwks"
|
||||
})
|
||||
class CustomerTransitStatisticsRouteContractTest {
|
||||
@Autowired private GatewayProperties gatewayProperties;
|
||||
|
||||
@@ -18,7 +18,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
"spring.cloud.nacos.discovery.register-enabled=false",
|
||||
"spring.cloud.nacos.config.enabled=false",
|
||||
"spring.cloud.gateway.discovery.locator.enabled=false",
|
||||
"baiyue.security.jwt-secret=test-jwt-secret-at-least-32-bytes-long",
|
||||
"baiyue.security.jwk-set-uri=http://127.0.0.1:1/jwks",
|
||||
"baiyue.cors.allowed-origins=http://localhost:5173,http://localhost:5174"
|
||||
})
|
||||
@AutoConfigureWebTestClient
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
"spring.cloud.nacos.discovery.register-enabled=false",
|
||||
"spring.cloud.nacos.config.enabled=false",
|
||||
"spring.cloud.gateway.discovery.locator.enabled=false",
|
||||
"baiyue.security.jwt-secret=test-jwt-secret-at-least-32-bytes-long"
|
||||
"baiyue.security.jwk-set-uri=http://127.0.0.1:1/jwks"
|
||||
})
|
||||
class GatewayFileRouteConfigurationTest {
|
||||
@Autowired
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.baiyue.gateway.config;
|
||||
|
||||
import com.nimbusds.jose.*;
|
||||
import com.nimbusds.jose.crypto.RSASSASigner;
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
|
||||
import com.nimbusds.jwt.JWTClaimsSet;
|
||||
import com.nimbusds.jwt.SignedJWT;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.oauth2.jwt.JwtException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
class UnifiedJwtDecoderTest {
|
||||
@Test
|
||||
void acceptsUnifiedRsaTokenAndRejectsWrongAudienceIssuerAndExpiry() throws Exception {
|
||||
var key = new RSAKeyGenerator(2048).keyID("current").generate();
|
||||
var server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
||||
server.createContext("/jwks", exchange -> {
|
||||
byte[] body = new JWKSet(key.toPublicJWK()).toString().getBytes(StandardCharsets.UTF_8);
|
||||
exchange.getResponseHeaders().add("Content-Type", "application/json");
|
||||
exchange.sendResponseHeaders(200, body.length);
|
||||
try (var output = exchange.getResponseBody()) { output.write(body); }
|
||||
});
|
||||
server.start();
|
||||
try {
|
||||
var properties = new GatewaySecurityProperties("http://127.0.0.1:" + server.getAddress().getPort() + "/jwks",
|
||||
"auth-service", "lastleg-api", "test:auth", List.of());
|
||||
var decoder = new GatewaySecurityConfig().reactiveJwtDecoder(properties);
|
||||
var signer = new RSASSASigner(key);
|
||||
String valid = token(signer, "auth-service", "lastleg-api", 120);
|
||||
assertThat(decoder.decode(valid).block(Duration.ofSeconds(5)).getSubject()).isEqualTo("42");
|
||||
for (String invalid : List.of(token(signer, "other", "lastleg-api", 120),
|
||||
token(signer, "auth-service", "other", 120), token(signer, "auth-service", "lastleg-api", -120))) {
|
||||
assertThatThrownBy(() -> decoder.decode(invalid).block(Duration.ofSeconds(5)))
|
||||
.isInstanceOf(JwtException.class);
|
||||
}
|
||||
} finally { server.stop(0); }
|
||||
}
|
||||
|
||||
private String token(RSASSASigner signer, String issuer, String audience, int ttl) throws Exception {
|
||||
var jwt = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.RS256).keyID("current").build(),
|
||||
new JWTClaimsSet.Builder().subject("42").issuer(issuer).audience(audience)
|
||||
.issueTime(Date.from(Instant.now().minusSeconds(300)))
|
||||
.expirationTime(Date.from(Instant.now().plusSeconds(ttl))).build());
|
||||
jwt.sign(signer);
|
||||
return jwt.serialize();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user