This commit is contained in:
2026-09-29 18:47:37 +08:00
parent 2c60a29e0f
commit c2927ab9a6
291 changed files with 4008 additions and 6298 deletions
@@ -20,6 +20,17 @@ public class RedisTokenSessionWebFilter implements WebFilter {
@Resource private ReactiveStringRedisTemplate reactiveStringRedisTemplate;
@Resource private GatewaySecurityProperties properties;
@Resource private ObjectMapper objectMapper;
@org.springframework.beans.factory.annotation.Value("${baiyue.security.session-validation-uri:http://auth:9100/internal/auth/accounts/sessions/validate}")
private String validationUri;
@org.springframework.beans.factory.annotation.Value("${baiyue.security.internal-service-token}")
private String internalToken;
private final org.springframework.web.reactive.function.client.WebClient stateClient =
org.springframework.web.reactive.function.client.WebClient.builder()
.clientConnector(new org.springframework.http.client.reactive.ReactorClientHttpConnector(
reactor.netty.http.client.HttpClient.create()
.option(io.netty.channel.ChannelOption.CONNECT_TIMEOUT_MILLIS, 1000)
.responseTimeout(java.time.Duration.ofSeconds(4)).followRedirect(false)))
.build();
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
@@ -34,7 +45,19 @@ public class RedisTokenSessionWebFilter implements WebFilter {
WebFilterChain chain) {
String jwtId = jwt.getToken().getId();
if (jwtId == null || jwtId.isBlank()) return unauthorized(exchange);
Object userId = jwt.getToken().getClaims().get("user_id");
if (userId == null) return unauthorized(exchange);
return reactiveStringRedisTemplate.hasKey(properties.accessTokenKey(jwtId))
.flatMap(active -> Boolean.TRUE.equals(active)
? stateClient.post().uri(validationUri)
.header("X-Internal-Service-Token", internalToken)
.bodyValue(java.util.Map.of("jwtId", jwtId, "userId", userId))
.retrieve().bodyToMono(com.fasterxml.jackson.databind.JsonNode.class)
.map(body -> body.path("code").isTextual() && "200".equals(body.path("code").asText())
&& body.path("data").isBoolean() && body.path("data").booleanValue())
.defaultIfEmpty(false)
: Mono.just(false))
.timeout(java.time.Duration.ofSeconds(4))
.onErrorReturn(false)
.flatMap(active -> Boolean.TRUE.equals(active)
? chain.filter(exchange) : unauthorized(exchange));
@@ -1,10 +0,0 @@
spring:
config:
activate:
on-profile: cn
baiyue:
deployment:
region-code: ${REGION_CODE:CN}
cell: ${DEPLOYMENT_CELL:cn}
data-residency: ${DATA_RESIDENCY:CN}
+10 -19
View File
@@ -3,7 +3,7 @@ server:
spring:
profiles:
active: ${SPRING_PROFILES_ACTIVE:dev}
active: ${SPRING_PROFILES_ACTIVE:test}
group:
dev: cn
test: cn
@@ -14,27 +14,27 @@ spring:
- org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
config:
import:
- optional:nacos:baiyue-common.yml?group=BAIYUE&refreshEnabled=true
- optional:nacos:${spring.application.name}.yml?group=BAIYUE&refreshEnabled=true
- nacos:baiyue-common.yml?group=${NACOS_GROUP:BAIYUE}&refreshEnabled=true
- nacos:${spring.application.name}.yml?group=${NACOS_GROUP:BAIYUE}&refreshEnabled=true
data:
redis:
host: ${REDIS_HOST:127.0.0.1}
host: ${REDIS_HOST}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}
database: ${REDIS_DATABASE:0}
timeout: ${REDIS_TIMEOUT:3s}
cloud:
nacos:
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}
username: ${NACOS_USERNAME:nacos}
password: ${NACOS_PASSWORD:nacos}
server-addr: ${NACOS_SERVER_ADDR}
username: ${NACOS_USERNAME}
password: ${NACOS_PASSWORD}
discovery:
enabled: ${NACOS_DISCOVERY_ENABLED:true}
register-enabled: ${NACOS_REGISTER_ENABLED:true}
group: ${NACOS_GROUP:BAIYUE}
namespace: ${NACOS_NAMESPACE:public}
namespace: ${NACOS_NAMESPACE}
config:
namespace: ${NACOS_NAMESPACE:public}
namespace: ${NACOS_NAMESPACE}
gateway:
discovery:
locator:
@@ -157,12 +157,6 @@ spring:
- Path=/integration-service/v3/api-docs/**
filters:
- StripPrefix=1
- id: event-bridge-openapi
uri: lb://event-bridge-service
predicates:
- Path=/event-bridge-service/v3/api-docs/**
filters:
- StripPrefix=1
- id: finance-openapi
uri: lb://finance-service
predicates:
@@ -178,7 +172,7 @@ baiyue:
allow-credentials: ${CORS_ALLOW_CREDENTIALS:false}
max-age: ${CORS_MAX_AGE:3600}
security:
jwk-set-uri: ${JWT_JWKS_URI:http://auth-service:9100/api/auth/.well-known/jwks.json}
jwk-set-uri: ${JWT_JWKS_URI:http://auth:9100/api/auth/.well-known/jwks.json}
issuer: ${JWT_ISSUER:auth-service}
audience: ${JWT_AUDIENCE:lastleg-api}
token-key-prefix: ${REDIS_TOKEN_KEY_PREFIX:baiyue:auth}
@@ -203,7 +197,6 @@ baiyue:
- /firstleg-service/v3/api-docs/**
- /audit-service/v3/api-docs/**
- /integration-service/v3/api-docs/**
- /event-bridge-service/v3/api-docs/**
- /finance-service/v3/api-docs/**
- /lastleg-service/v3/api-docs/**
- /warehouse-overseas-service/v3/api-docs/**
@@ -251,8 +244,6 @@ springdoc:
url: /audit-service/v3/api-docs
- name: integration
url: /integration-service/v3/api-docs
- name: event-bridge
url: /event-bridge-service/v3/api-docs
- name: finance
url: /finance-service/v3/api-docs
- name: lastleg
@@ -0,0 +1,58 @@
package com.baiyue.gateway.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sun.net.httpserver.HttpServer;
import org.junit.jupiter.api.Test;
import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.test.util.ReflectionTestUtils;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
class RedisTokenSessionWebFilterTest {
@Test void checksAuthorityAsynchronouslyAndFailsClosed() throws Exception {
var response = new AtomicReference<>("{\"code\":\"200\",\"data\":true}");
var bearer = new AtomicReference<String>();
var server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
server.createContext("/validate", exchange -> {
bearer.set(exchange.getRequestHeaders().getFirst("Authorization"));
byte[] bytes = response.get().getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().add("Content-Type", "application/json");
exchange.sendResponseHeaders(200, bytes.length); exchange.getResponseBody().write(bytes); exchange.close();
});
server.start();
var filter = new RedisTokenSessionWebFilter();
var redis = mock(ReactiveStringRedisTemplate.class);
when(redis.hasKey("test:access:jti")).thenReturn(Mono.just(true));
ReflectionTestUtils.setField(filter, "reactiveStringRedisTemplate", redis);
ReflectionTestUtils.setField(filter, "properties", new GatewaySecurityProperties(null, "issuer", "aud", "test", List.of()));
ReflectionTestUtils.setField(filter, "objectMapper", new ObjectMapper());
ReflectionTestUtils.setField(filter, "validationUri", "http://127.0.0.1:" + server.getAddress().getPort() + "/validate");
ReflectionTestUtils.setField(filter, "internalToken", "internal");
try {
for (boolean accepted : new boolean[]{true, false}) {
response.set("{\"code\":\"200\",\"data\":" + accepted + "}");
var jwt = Jwt.withTokenValue("token").header("alg", "RS256").claim("jti", "jti").claim("user_id", 1L).build();
var exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/api/platform/orders"))
.mutate().principal(Mono.just(new JwtAuthenticationToken(jwt))).build();
var invoked = new AtomicBoolean();
StepVerifier.create(filter.filter(exchange, request -> { invoked.set(true); return Mono.empty(); }))
.expectComplete().verify(Duration.ofSeconds(5));
assertThat(invoked.get()).isEqualTo(accepted);
if (!accepted) assertThat(exchange.getResponse().getStatusCode().value()).isEqualTo(401);
assertThat(bearer.get()).isNull();
}
} finally { server.stop(0); }
}
}