fix: project overseas authorization outside jwt

This commit is contained in:
2026-09-07 11:37:06 +08:00
parent 6b479d1a68
commit 4963889024
33 changed files with 382 additions and 92 deletions
@@ -5,14 +5,13 @@ 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", "token_version", "permission_version", "authorities");
"sub", "tenant_id", "app_code", "token_version", "permission_version");
@Override
public OAuth2TokenValidatorResult validate(Jwt jwt) {
@@ -44,11 +43,6 @@ final class RequiredClaimsJwtValidator implements OAuth2TokenValidator<Jwt> {
&& !("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();
}
@@ -6,6 +6,8 @@ import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.util.LinkedHashSet;
import java.util.Set;
@Component
public class HttpOverseasSecurityValidationClient implements OverseasSecurityValidationClient {
@@ -29,9 +31,20 @@ public class HttpOverseasSecurityValidationClient implements OverseasSecurityVal
return Mono.error(new IllegalStateException("海外安全投影响应无效"));
}
return Mono.just(new Result(data.path("valid").asBoolean(false),
data.path("failureReason").isNull() ? null : data.path("failureReason").asText(null)));
data.path("failureReason").isNull() ? null : data.path("failureReason").asText(null),
strings(data.path("roles")), strings(data.path("authorities"))));
});
}
private static Set<String> strings(JsonNode values) {
if (values == null || !values.isArray()) return Set.of();
LinkedHashSet<String> result = new LinkedHashSet<>();
values.forEach(value -> {
String text = value.asText("");
if (!text.isBlank()) result.add(text);
});
return Set.copyOf(result);
}
private record Request(long userId, long tenantId, long permissionVersion) { }
}
@@ -1,6 +1,7 @@
package com.baiyue.gateway.filter;
import com.baiyue.gateway.config.GatewaySecurityConfig;
import com.baiyue.gateway.config.OverseasSecurityProjectionProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
@@ -11,15 +12,20 @@ import org.springframework.security.oauth2.server.resource.authentication.JwtAut
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Collection;
import java.util.stream.Collectors;
@Component
public class OverseasSecurityProjectionFilter implements GlobalFilter, Ordered {
private final OverseasSecurityValidationClient client;
private final ObjectMapper objectMapper;
private final OverseasSecurityProjectionProperties properties;
public OverseasSecurityProjectionFilter(OverseasSecurityValidationClient client, ObjectMapper objectMapper) {
public OverseasSecurityProjectionFilter(OverseasSecurityValidationClient client, ObjectMapper objectMapper,
OverseasSecurityProjectionProperties properties) {
this.client = client;
this.objectMapper = objectMapper;
this.properties = properties;
}
@Override
@@ -45,7 +51,14 @@ public class OverseasSecurityProjectionFilter implements GlobalFilter, Ordered {
return client.validate(userId, tenantId, permissionVersion)
.switchIfEmpty(Mono.error(new SecurityProjectionFailure("A0401", "海外安全状态校验不可用")))
.flatMap(result -> {
if (result.valid()) return chain.filter(exchange);
if (result.valid()) {
var request = exchange.getRequest().mutate()
.header("X-Overseas-Roles", join(result.roles()))
.header("X-Overseas-Authorities", join(result.authorities()))
.header("X-Gateway-Internal-Token", properties.internalServiceToken())
.build();
return chain.filter(exchange.mutate().request(request).build());
}
if ("USER_DISABLED".equals(result.failureReason())) {
return Mono.error(new SecurityProjectionFailure("A0401", "账号已停用"));
}
@@ -64,6 +77,10 @@ public class OverseasSecurityProjectionFilter implements GlobalFilter, Ordered {
catch (NumberFormatException exception) { return null; }
}
private static String join(Collection<String> values) {
return values.stream().sorted().collect(Collectors.joining(","));
}
@Override public int getOrder() { return -95; }
private static final class SecurityProjectionFailure extends RuntimeException {
@@ -1,8 +1,14 @@
package com.baiyue.gateway.filter;
import reactor.core.publisher.Mono;
import java.util.Set;
public interface OverseasSecurityValidationClient {
Mono<Result> validate(long userId, long tenantId, long permissionVersion);
record Result(boolean valid, String failureReason) { }
record Result(boolean valid, String failureReason, Set<String> roles, Set<String> authorities) {
public Result {
roles = roles == null ? Set.of() : Set.copyOf(roles);
authorities = authorities == null ? Set.of() : Set.copyOf(authorities);
}
}
}
@@ -18,7 +18,8 @@ import java.util.List;
public class TrustedIdentityHeaderFilter implements GlobalFilter, Ordered {
private static final List<String> TRUSTED_HEADERS = List.of(
"X-User-Id", "X-Membership-Id", "X-Tenant-Id", "X-Operator-Tenant-Id",
"X-App-Code", "X-Client-Type"
"X-App-Code", "X-Client-Type", "X-Overseas-Roles", "X-Overseas-Authorities",
"X-Gateway-Internal-Token"
);
@Override
@@ -70,4 +71,3 @@ public class TrustedIdentityHeaderFilter implements GlobalFilter, Ordered {
@Override public String getName() { return "anonymous"; }
}
}