This commit is contained in:
2026-08-05 16:26:10 +08:00
parent 06f7f9d2f2
commit 5ba9716e12
155 changed files with 2883 additions and 551 deletions
+1 -1
View File
@@ -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"));
+1 -1
View File
@@ -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:
@@ -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")));
@@ -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()