优化
This commit is contained in:
+1
-1
@@ -3,7 +3,7 @@
|
||||
## 职责边界
|
||||
|
||||
- 统一承接前端流量,负责服务路由、CORS、JWT/Redis 会话校验、商户端与平台端访问域隔离及 Swagger 聚合。
|
||||
- 网关完成入口认证,但业务服务仍必须执行租户、数据范围和权限校验;业务编排不得写入网关。
|
||||
- 网关完成入口认证,但业务服务仍必须执行客户归属、数据范围和权限校验;业务编排不得写入网关。Claim 中的 `tenant_id > 0` 当前是客户隔离兼容字段,`tenant_id=0` 是平台技术作用域,不得据此新增租户切换或真实租户访问域。
|
||||
|
||||
## 快速索引
|
||||
|
||||
|
||||
@@ -26,7 +26,18 @@ final class AppScopeAuthorizationManager implements ReactiveAuthorizationManager
|
||||
}
|
||||
|
||||
private boolean hasExpectedScope(Authentication authentication) {
|
||||
return authentication instanceof JwtAuthenticationToken jwt
|
||||
&& expectedAppCode.equals(jwt.getToken().getClaimAsString("app_code"));
|
||||
if (!(authentication instanceof JwtAuthenticationToken jwt)
|
||||
|| !expectedAppCode.equals(jwt.getToken().getClaimAsString("app_code"))) {
|
||||
return false;
|
||||
}
|
||||
Object tenantIdClaim = jwt.getToken().getClaim("tenant_id");
|
||||
if (!(tenantIdClaim instanceof Number tenantId)) {
|
||||
return false;
|
||||
}
|
||||
return switch (expectedAppCode) {
|
||||
case "PLATFORM" -> tenantId.longValue() == 0L;
|
||||
case "MERCHANT" -> tenantId.longValue() > 0L;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ public class GatewaySecurityConfig {
|
||||
CorsConfigurationSource corsConfigurationSource) {
|
||||
var exchanges = http.authorizeExchange();
|
||||
exchanges.pathMatchers(HttpMethod.OPTIONS, "/**").permitAll();
|
||||
exchanges.pathMatchers("/api/iam/**").denyAll();
|
||||
properties.permitPaths().forEach(path -> exchanges.pathMatchers(path).permitAll());
|
||||
exchanges.pathMatchers("/api/merchant/**")
|
||||
.access(new AppScopeAuthorizationManager("MERCHANT"));
|
||||
|
||||
@@ -49,7 +49,7 @@ spring:
|
||||
- id: iam-service
|
||||
uri: lb://iam-service
|
||||
predicates:
|
||||
- Path=/api/iam/**,/api/platform/v1/iam/**,/api/merchant/v1/iam/**
|
||||
- Path=/api/platform/v1/iam/**,/api/merchant/v1/iam/**
|
||||
- id: audit-service
|
||||
uri: lb://audit-service
|
||||
predicates:
|
||||
|
||||
+28
-3
@@ -15,7 +15,15 @@ class AppScopeAuthorizationManagerTest {
|
||||
@Test
|
||||
void acceptsMatchingApplicationScope() {
|
||||
var manager = new AppScopeAuthorizationManager("MERCHANT");
|
||||
StepVerifier.create(manager.check(Mono.just(authentication("MERCHANT")), null))
|
||||
StepVerifier.create(manager.check(Mono.just(authentication("MERCHANT", 10001L)), null))
|
||||
.expectNextMatches(decision -> decision.isGranted())
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void acceptsPlatformOnlyInPlatformTenant() {
|
||||
var manager = new AppScopeAuthorizationManager("PLATFORM");
|
||||
StepVerifier.create(manager.check(Mono.just(authentication("PLATFORM", 0L)), null))
|
||||
.expectNextMatches(decision -> decision.isGranted())
|
||||
.verifyComplete();
|
||||
}
|
||||
@@ -23,7 +31,23 @@ class AppScopeAuthorizationManagerTest {
|
||||
@Test
|
||||
void rejectsDifferentApplicationScope() {
|
||||
var manager = new AppScopeAuthorizationManager("PLATFORM");
|
||||
StepVerifier.create(manager.check(Mono.just(authentication("MERCHANT")), null))
|
||||
StepVerifier.create(manager.check(Mono.just(authentication("MERCHANT", 10001L)), null))
|
||||
.expectNextMatches(decision -> !decision.isGranted())
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsPlatformApplicationWithCustomerTenant() {
|
||||
var manager = new AppScopeAuthorizationManager("PLATFORM");
|
||||
StepVerifier.create(manager.check(Mono.just(authentication("PLATFORM", 10001L)), null))
|
||||
.expectNextMatches(decision -> !decision.isGranted())
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsMerchantApplicationWithoutPositiveCustomerTenant() {
|
||||
var manager = new AppScopeAuthorizationManager("MERCHANT");
|
||||
StepVerifier.create(manager.check(Mono.just(authentication("MERCHANT", 0L)), null))
|
||||
.expectNextMatches(decision -> !decision.isGranted())
|
||||
.verifyComplete();
|
||||
}
|
||||
@@ -43,13 +67,14 @@ class AppScopeAuthorizationManagerTest {
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
private JwtAuthenticationToken authentication(String appCode) {
|
||||
private JwtAuthenticationToken authentication(String appCode, long tenantId) {
|
||||
Jwt jwt = Jwt.withTokenValue("token")
|
||||
.header("alg", "none")
|
||||
.subject("1")
|
||||
.issuedAt(Instant.now())
|
||||
.expiresAt(Instant.now().plusSeconds(60))
|
||||
.claim("app_code", appCode)
|
||||
.claim("tenant_id", tenantId)
|
||||
.build();
|
||||
return new JwtAuthenticationToken(jwt,
|
||||
java.util.List.of(new SimpleGrantedAuthority("authenticated")));
|
||||
|
||||
+16
@@ -35,6 +35,22 @@ class GatewayFileRouteConfigurationTest {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void iamRouteDoesNotExposeUnscopedCompatibilityPath() {
|
||||
var route = gatewayProperties.getRoutes().stream()
|
||||
.filter(candidate -> "iam-service".equals(candidate.getId()))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
|
||||
assertThat(route.getPredicates()).singleElement().satisfies(predicate -> {
|
||||
assertThat(predicate.getName()).isEqualTo("Path");
|
||||
assertThat(String.join(",", predicate.getArgs().values()))
|
||||
.contains("/api/platform/v1/iam/**")
|
||||
.contains("/api/merchant/v1/iam/**")
|
||||
.doesNotContain("/api/iam/**");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void warehouseResourceRequestsRouteToWarehouseService() {
|
||||
var route = gatewayProperties.getRoutes().stream()
|
||||
|
||||
Reference in New Issue
Block a user