feat: gate overseas receiving and harden phase4 workflows

This commit is contained in:
2026-08-25 10:31:04 +08:00
parent cb78bc6b41
commit 52b9fc382f
93 changed files with 2779 additions and 201 deletions
@@ -0,0 +1,43 @@
package com.baiyue.gateway.config;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
class PdaSessionValidationDeploymentContractTest {
@Test
void nacosComposeAndJenkinsDeploymentRequireOnlyEnvironmentBackedAuthCoordinates() throws Exception {
String application = read("gateway-overseas/src/main/resources/application.yml");
String nacos = read("config/nacos/overseas/baiyue-common.yml");
String compose = read("deploy/docker-compose.overseas.yml");
String deployServices = read("deploy/scripts/deploy-services.sh");
String deployBuiltService = read("deploy/scripts/deploy-built-service.sh");
String jenkins = read("Jenkinsfile");
for (String source : new String[]{application, nacos, compose}) {
assertThat(source).contains("AUTH_SESSION_VALIDATION_URL", "INTERNAL_SERVICE_TOKEN",
"PDA_SESSION_VALIDATION_TIMEOUT");
}
assertThat(deployServices).contains("gateway-overseas", "AUTH_SESSION_VALIDATION_URL");
assertThat(deployBuiltService).contains("gateway-overseas", "AUTH_SESSION_VALIDATION_URL");
assertThat(jenkins).contains("'gateway-overseas': 'SERVICE_GATEWAY_OVERSEAS'", "'auth': 'SERVICE_AUTH'",
"'iam': 'SERVICE_IAM'");
assertThat(application).doesNotContain("/internal/auth/accounts/overseas-session/validate");
assertThat(read("gateway-overseas/src/main/resources/application-overseas.yml"))
.doesNotContain("/internal/auth/**", "/internal/auth/accounts/overseas-session/validate");
assertThat(read("gateway-overseas/src/main/java/com/baiyue/gateway/filter/HttpPdaSessionValidationClient.java"))
.contains("new Request(userId, tokenVersion, overseasWarehouseId)",
".header(\"X-Internal-Service-Token\"")
.doesNotContain("Authorization", "accessToken", "rawToken", "getTokenValue");
}
private static String read(String relative) throws Exception {
Path path = Path.of(relative);
if (!Files.exists(path)) path = Path.of("..").resolve(relative);
return Files.readString(path, StandardCharsets.UTF_8);
}
}
@@ -21,11 +21,22 @@ class RequiredClaimsJwtValidatorTest {
assertThat(validator.validate(jwt(false)).hasErrors()).isTrue();
}
@Test
void rejectsMissingAccountTokenVersion() {
Jwt value = Jwt.withTokenValue("token").header("alg", "RS256").subject("1001")
.issuedAt(Instant.now()).expiresAt(Instant.now().plusSeconds(300))
.claim("tenant_id", "0").claim("app_code", "PLATFORM")
.claim("permission_version", 3L).claim("authorities", List.of())
.build();
assertThat(validator.validate(value).hasErrors()).isTrue();
}
@Test
void rejectsMissingAuthorities() {
Jwt value = Jwt.withTokenValue("token").header("alg", "RS256").subject("1001")
.issuedAt(Instant.now()).expiresAt(Instant.now().plusSeconds(300))
.claim("tenant_id", "0").claim("app_code", "PLATFORM").claim("permission_version", 3L)
.claim("tenant_id", "0").claim("app_code", "PLATFORM").claim("token_version", 1)
.claim("permission_version", 3L)
.build();
assertThat(validator.validate(value).hasErrors()).isTrue();
}
@@ -54,6 +65,7 @@ class RequiredClaimsJwtValidatorTest {
Jwt value = Jwt.withTokenValue("token").header("alg", "RS256").subject("not-a-user")
.issuedAt(Instant.now()).expiresAt(Instant.now().plusSeconds(300))
.claim("tenant_id", "0").claim("app_code", "PLATFORM").claim("permission_version", -1L)
.claim("token_version", 1)
.claim("authorities", List.of()).build();
assertThat(validator.validate(value).hasErrors()).isTrue();
}
@@ -72,6 +84,7 @@ class RequiredClaimsJwtValidatorTest {
.expiresAt(Instant.now().plusSeconds(300))
.claim("tenant_id", tenantId)
.claim("app_code", appCode)
.claim("token_version", 1)
.claim("authorities", authorities);
if (includePermissionVersion) {
builder.claim("permission_version", 3L);
@@ -0,0 +1,119 @@
package com.baiyue.gateway.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpStatus;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
class PdaSessionValidationFilterTest {
private final PdaSessionValidationClient client = mock(PdaSessionValidationClient.class);
private final PdaSessionValidationFilter filter = new PdaSessionValidationFilter(client, new ObjectMapper());
@Test
void oldTokenIsRejectedImmediatelyAfterWarehouseChange() {
when(client.validate(42L, 7, 900L)).thenReturn(Mono.just(
new PdaSessionValidationClient.Result(false, "TOKEN_VERSION_MISMATCH")));
var exchange = exchange("/api/pda/v1/last-leg/receiving/containers", claims(7, 900L));
StepVerifier.create(filter.filter(exchange, ignored -> Mono.empty())).verifyComplete();
assertThat(exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
assertThat(body(exchange)).contains("A0401", "令牌已失效");
}
@Test
void newlyIssuedTokenWithCurrentWarehousePasses() {
when(client.validate(42L, 8, 901L)).thenReturn(Mono.just(
new PdaSessionValidationClient.Result(true, null)));
var exchange = exchange("/api/pda/v1/last-leg/receiving/containers", claims(8, 901L));
AtomicBoolean routed = new AtomicBoolean();
StepVerifier.create(filter.filter(exchange, ignored -> {
routed.set(true);
return Mono.empty();
})).verifyComplete();
assertThat(routed).isTrue();
assertThat(exchange.getResponse().getStatusCode()).isNull();
}
@Test
void unboundWarehouseIsDeniedWithoutCallingAuth() {
var exchange = exchange("/api/pda/v1/last-leg/receiving/containers", claims(8, null));
StepVerifier.create(filter.filter(exchange, ignored -> Mono.empty())).verifyComplete();
assertThat(exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
assertThat(body(exchange)).contains("A0403", "未绑定仓库");
verifyNoInteractions(client);
}
@Test
void authUnavailableFailsClosed() {
when(client.validate(42L, 8, 901L)).thenReturn(Mono.error(new IllegalStateException("timeout")));
var exchange = exchange("/api/pda/v1/last-leg/receiving/containers", claims(8, 901L));
StepVerifier.create(filter.filter(exchange, ignored -> Mono.empty())).verifyComplete();
assertThat(exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
assertThat(body(exchange)).contains("A0401", "校验不可用");
}
@Test
void ordinaryOverseasAdminAndClientPathsDoNotSynchronouslyValidate() {
for (String path : new String[]{"/api/platform/v1/last-leg/orders", "/api/merchant/v1/last-leg/orders"}) {
var exchange = exchange(path, claims(7, 900L));
AtomicBoolean routed = new AtomicBoolean();
StepVerifier.create(filter.filter(exchange, ignored -> {
routed.set(true);
return Mono.empty();
})).verifyComplete();
assertThat(routed).isTrue();
}
verifyNoInteractions(client);
}
private static ServerWebExchange exchange(String path, Map<String, Object> claims) {
Jwt.Builder jwt = Jwt.withTokenValue("already-verified-by-resource-server")
.header("alg", "RS256")
.subject("42")
.issuedAt(Instant.now().minusSeconds(5))
.expiresAt(Instant.now().plusSeconds(300));
claims.forEach(jwt::claim);
MockServerWebExchange exchange = MockServerWebExchange.from(
MockServerHttpRequest.get(path).build());
return exchange.mutate()
.principal(Mono.just(new JwtAuthenticationToken(jwt.build(),
List.of(new SimpleGrantedAuthority("ROLE_WAREHOUSE_PICKER")))))
.build();
}
private static Map<String, Object> claims(int tokenVersion, Long warehouseId) {
var claims = new java.util.LinkedHashMap<String, Object>();
claims.put("token_version", tokenVersion);
if (warehouseId != null) claims.put("overseas_warehouse_id", warehouseId);
return claims;
}
private static String body(ServerWebExchange exchange) {
return ((org.springframework.mock.http.server.reactive.MockServerHttpResponse) exchange.getResponse())
.getBodyAsString().block();
}
}