This commit is contained in:
2026-08-20 18:02:38 +08:00
parent b55b049e77
commit 1cd8b6a473
210 changed files with 6974 additions and 825 deletions
@@ -53,7 +53,6 @@ public class GatewaySecurityConfig {
.withJwkSetUri(properties.jwkSetUri())
.jwsAlgorithms(algorithms -> {
algorithms.add(SignatureAlgorithm.RS256);
algorithms.add(SignatureAlgorithm.ES256);
})
.build();
decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(
@@ -5,12 +5,14 @@ import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jwt.Jwt;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
/** Validates the identity and authorization claims required by overseas APIs. */
final class RequiredClaimsJwtValidator implements OAuth2TokenValidator<Jwt> {
private static final List<String> REQUIRED_CLAIMS = List.of(
"sub", "tenant_id", "app_code", "permission_version");
"sub", "tenant_id", "app_code", "permission_version", "authorities");
@Override
public OAuth2TokenValidatorResult validate(Jwt jwt) {
@@ -21,6 +23,42 @@ final class RequiredClaimsJwtValidator implements OAuth2TokenValidator<Jwt> {
"invalid_token", "JWT 缺少必要声明: " + claim, null));
}
}
Long subject = longValue(jwt.getSubject());
Long tenantId = longValue(jwt.getClaims().get("tenant_id"));
Long permissionVersion = longValue(jwt.getClaims().get("permission_version"));
String appCode = String.valueOf(jwt.getClaims().get("app_code")).toUpperCase(Locale.ROOT);
if (subject == null || subject <= 0) {
return failure("JWT sub 必须是正整数用户 ID");
}
if (tenantId == null || tenantId < 0) {
return failure("JWT tenant_id 必须是非负整数客户范围");
}
if (permissionVersion == null || permissionVersion < 0) {
return failure("JWT permission_version 必须是非负整数");
}
if (!("PLATFORM".equals(appCode) && tenantId == 0L)
&& !("MERCHANT".equals(appCode) && tenantId > 0L)) {
return failure("JWT app_code 与 tenant_id 访问域不一致");
}
Object authorities = jwt.getClaims().get("authorities");
if (!(authorities instanceof Collection<?> values)
|| values.stream().anyMatch(value -> value == null || value.toString().isBlank())) {
return failure("JWT authorities 必须是权限列表");
}
return OAuth2TokenValidatorResult.success();
}
private static Long longValue(Object value) {
if (value instanceof Number number) return number.longValue();
if (value == null) return null;
try {
return Long.valueOf(value.toString());
} catch (NumberFormatException ignored) {
return null;
}
}
private static OAuth2TokenValidatorResult failure(String message) {
return OAuth2TokenValidatorResult.failure(new OAuth2Error("invalid_token", message, null));
}
}
@@ -59,7 +59,9 @@ springdoc:
swagger-ui:
urls:
- { name: lastleg-merchant, url: /lastleg-service/v3/api-docs/merchant }
- { name: lastleg-platform, url: /lastleg-service/v3/api-docs/platform }
- { name: warehouse-merchant, url: /warehouse-overseas-service/v3/api-docs/merchant }
- { name: warehouse-platform, url: /warehouse-overseas-service/v3/api-docs/platform }
- { name: file-platform, url: /file-service/v3/api-docs/platform }
- { name: file-merchant, url: /file-service/v3/api-docs/merchant }
- { name: print-platform, url: /print-service/v3/api-docs/platform }
@@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.security.oauth2.jwt.Jwt;
import java.time.Instant;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@@ -20,14 +21,58 @@ class RequiredClaimsJwtValidatorTest {
assertThat(validator.validate(jwt(false)).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)
.build();
assertThat(validator.validate(value).hasErrors()).isTrue();
}
@Test
void acceptsEmptyAuthoritySnapshotAndLeavesAuthorizationToControllers() {
assertThat(validator.validate(jwt("0", "PLATFORM", List.of(), true)).hasErrors()).isFalse();
}
@Test
void acceptsMerchantIdentityOnlyWithPositiveCustomerScope() {
assertThat(validator.validate(jwt("2001", "MERCHANT", List.of("merchant:order:list"), true)).hasErrors())
.isFalse();
}
@Test
void rejectsApplicationAndCustomerScopeMismatch() {
assertThat(validator.validate(jwt("2001", "PLATFORM", List.of("platform:test:list"), true)).hasErrors())
.isTrue();
assertThat(validator.validate(jwt("0", "MERCHANT", List.of("merchant:test:list"), true)).hasErrors())
.isTrue();
}
@Test
void rejectsMalformedIdentityAndPermissionVersion() {
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("authorities", List.of()).build();
assertThat(validator.validate(value).hasErrors()).isTrue();
}
private Jwt jwt(boolean includePermissionVersion) {
return jwt("0", "PLATFORM",
List.of("platform:overseas-warehouse:inventory:list"), includePermissionVersion);
}
private Jwt jwt(String tenantId, String appCode, List<String> authorities,
boolean includePermissionVersion) {
Jwt.Builder builder = Jwt.withTokenValue("token")
.header("alg", "RS256")
.subject("1001")
.issuedAt(Instant.now())
.expiresAt(Instant.now().plusSeconds(300))
.claim("tenant_id", "2001")
.claim("app_code", "PLATFORM");
.claim("tenant_id", tenantId)
.claim("app_code", appCode)
.claim("authorities", authorities);
if (includePermissionVersion) {
builder.claim("permission_version", 3L);
}