From f9a1115cef60523398e429d84a55fbe3be5f1964 Mon Sep 17 00:00:00 2001 From: PeterMue <3819198+PeterMue@users.noreply.github.com> Date: Tue, 22 Apr 2025 16:19:50 +0200 Subject: [PATCH] fixes gh-3774 Configure CORS after refresh routes completed on RefreshRoutesResultEvent Signed-off-by: PeterMue <3819198+PeterMue@users.noreply.github.com> --- .../CorsGatewayFilterApplicationListener.java | 8 +- ...wayControllerEndpointRedisRefreshTest.java | 148 ++++++++++++++++++ 2 files changed, 152 insertions(+), 4 deletions(-) create mode 100644 spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpointRedisRefreshTest.java diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/cors/CorsGatewayFilterApplicationListener.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/cors/CorsGatewayFilterApplicationListener.java index 337588b82b..ebe3e148f0 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/cors/CorsGatewayFilterApplicationListener.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/cors/CorsGatewayFilterApplicationListener.java @@ -25,7 +25,7 @@ import java.util.concurrent.atomic.AtomicReference; import org.springframework.cloud.gateway.config.GlobalCorsProperties; -import org.springframework.cloud.gateway.event.RefreshRoutesEvent; +import org.springframework.cloud.gateway.event.RefreshRoutesResultEvent; import org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping; import org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory; import org.springframework.cloud.gateway.route.Route; @@ -34,14 +34,14 @@ import org.springframework.web.cors.CorsConfiguration; /** - * This class updates Cors configuration each time a {@link RefreshRoutesEvent} is + * This class updates Cors configuration each time a {@link RefreshRoutesResultEvent} is * consumed. The {@link Route}'s predicates are inspected for a * {@link PathRoutePredicateFactory} and the first pattern is used. * * @author Fredrich Ombico * @author Abel Salgado Romero */ -public class CorsGatewayFilterApplicationListener implements ApplicationListener { +public class CorsGatewayFilterApplicationListener implements ApplicationListener { private final GlobalCorsProperties globalCorsProperties; @@ -61,7 +61,7 @@ public CorsGatewayFilterApplicationListener(GlobalCorsProperties globalCorsPrope } @Override - public void onApplicationEvent(RefreshRoutesEvent event) { + public void onApplicationEvent(RefreshRoutesResultEvent event) { routeLocator.getRoutes().collectList().subscribe(routes -> { // pre-populate with pre-existing global cors configurations to combine with. var corsConfigurations = new HashMap<>(globalCorsProperties.getCorsConfigurations()); diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpointRedisRefreshTest.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpointRedisRefreshTest.java new file mode 100644 index 0000000000..1d630bac67 --- /dev/null +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpointRedisRefreshTest.java @@ -0,0 +1,148 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.actuate; + +import java.net.URI; +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition; +import org.springframework.cloud.gateway.route.RouteDefinition; +import org.springframework.http.MediaType; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.reactive.function.BodyInserters; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +/** + * @author Peter Müller + */ +@SpringBootTest(properties = {"management.endpoint.gateway.enabled=true", + "management.endpoints.web.exposure.include=*", "spring.cloud.gateway.actuator.verbose.enabled=true"}, + webEnvironment = RANDOM_PORT) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@ActiveProfiles("redis-route-repository") +@Testcontainers +@Tag("DockerRequired") +public class GatewayControllerEndpointRedisRefreshTest { + + @Container + public static GenericContainer redis = new GenericContainer<>("redis:5.0.14-alpine").withExposedPorts(6379); + + @BeforeAll + public static void startRedisContainer() { + redis.start(); + } + + @DynamicPropertySource + static void containerProperties(DynamicPropertyRegistry registry) { + registry.add("spring.data.redis.host", redis::getHost); + registry.add("spring.data.redis.port", redis::getFirstMappedPort); + } + + @Autowired + WebTestClient testClient; + + @LocalServerPort + int port; + + @Test + public void testCorsConfigurationAfterReload() { + Map cors = new HashMap<>(); + cors.put("allowCredentials", false); + cors.put("allowedOrigins", "*"); + cors.put("allowedMethods", "GET"); + + createOrUpdateRouteWithCors(cors); + + Awaitility.await().atMost(Duration.ofSeconds(3)).untilAsserted(() -> assertRouteHasCorsConfig(cors)); + Awaitility.await().atMost(Duration.ofSeconds(3)).untilAsserted(() -> assertPreflightAllowOrigin("*")); + + cors.put("allowedOrigins", "http://example.org"); + createOrUpdateRouteWithCors(cors); + + Awaitility.await().atMost(Duration.ofSeconds(3)).untilAsserted(() -> assertRouteHasCorsConfig(cors)); + Awaitility.await().atMost(Duration.ofSeconds(3)) + .untilAsserted(() -> assertPreflightAllowOrigin("http://example.org")); + } + + void createOrUpdateRouteWithCors(Map cors) { + RouteDefinition testRouteDefinition = new RouteDefinition(); + testRouteDefinition.setUri(URI.create("http://example.org")); + + PredicateDefinition methodRoutePredicateDefinition = new PredicateDefinition("Method=GET"); + testRouteDefinition.setPredicates(List.of(methodRoutePredicateDefinition)); + + testRouteDefinition.setMetadata(Map.of("cors", cors)); + + testClient.post() + .uri("http://localhost:" + port + "/actuator/gateway/routes/cors-test-route") + .accept(MediaType.APPLICATION_JSON) + .body(BodyInserters.fromValue(testRouteDefinition)) + .exchange() + .expectStatus() + .isCreated(); + + testClient.post() + .uri("http://localhost:" + port + "/actuator/gateway/refresh") + .exchange() + .expectStatus() + .isOk(); + } + + void assertRouteHasCorsConfig(Map cors) { + testClient.get() + .uri("http://localhost:" + port + "/actuator/gateway/routes/cors-test-route") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .jsonPath("$.metadata") + .value(map -> assertThat((Map) map).hasSize(1) + .containsEntry("cors", cors)); + } + + void assertPreflightAllowOrigin(String origin) { + testClient.options() + .uri("http://localhost:" + port + "/") + .header("Origin", "http://example.org") + .header("Access-Control-Request-Method", "GET") + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .valueEquals("Access-Control-Allow-Origin", origin); + } +}