优化
This commit is contained in:
@@ -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}
|
||||
@@ -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); }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user