Skip to content

Commit f9a1115

Browse files
committed
fixes gh-3774
Configure CORS after refresh routes completed on RefreshRoutesResultEvent Signed-off-by: PeterMue <3819198+PeterMue@users.noreply.github.com>
1 parent 00a6001 commit f9a1115

File tree

2 files changed

+152
-4
lines changed

2 files changed

+152
-4
lines changed

spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/cors/CorsGatewayFilterApplicationListener.java

+4-4
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
import java.util.concurrent.atomic.AtomicReference;
2626

2727
import org.springframework.cloud.gateway.config.GlobalCorsProperties;
28-
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
28+
import org.springframework.cloud.gateway.event.RefreshRoutesResultEvent;
2929
import org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping;
3030
import org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory;
3131
import org.springframework.cloud.gateway.route.Route;
@@ -34,14 +34,14 @@
3434
import org.springframework.web.cors.CorsConfiguration;
3535

3636
/**
37-
* This class updates Cors configuration each time a {@link RefreshRoutesEvent} is
37+
* This class updates Cors configuration each time a {@link RefreshRoutesResultEvent} is
3838
* consumed. The {@link Route}'s predicates are inspected for a
3939
* {@link PathRoutePredicateFactory} and the first pattern is used.
4040
*
4141
* @author Fredrich Ombico
4242
* @author Abel Salgado Romero
4343
*/
44-
public class CorsGatewayFilterApplicationListener implements ApplicationListener<RefreshRoutesEvent> {
44+
public class CorsGatewayFilterApplicationListener implements ApplicationListener<RefreshRoutesResultEvent> {
4545

4646
private final GlobalCorsProperties globalCorsProperties;
4747

@@ -61,7 +61,7 @@ public CorsGatewayFilterApplicationListener(GlobalCorsProperties globalCorsPrope
6161
}
6262

6363
@Override
64-
public void onApplicationEvent(RefreshRoutesEvent event) {
64+
public void onApplicationEvent(RefreshRoutesResultEvent event) {
6565
routeLocator.getRoutes().collectList().subscribe(routes -> {
6666
// pre-populate with pre-existing global cors configurations to combine with.
6767
var corsConfigurations = new HashMap<>(globalCorsProperties.getCorsConfigurations());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*
2+
* Copyright 2013-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.gateway.actuate;
18+
19+
import java.net.URI;
20+
import java.time.Duration;
21+
import java.util.HashMap;
22+
import java.util.List;
23+
import java.util.Map;
24+
25+
import org.awaitility.Awaitility;
26+
import org.junit.jupiter.api.BeforeAll;
27+
import org.junit.jupiter.api.Tag;
28+
import org.junit.jupiter.api.Test;
29+
import org.testcontainers.containers.GenericContainer;
30+
import org.testcontainers.junit.jupiter.Container;
31+
import org.testcontainers.junit.jupiter.Testcontainers;
32+
33+
import org.springframework.beans.factory.annotation.Autowired;
34+
import org.springframework.boot.test.context.SpringBootTest;
35+
import org.springframework.boot.test.web.server.LocalServerPort;
36+
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
37+
import org.springframework.cloud.gateway.route.RouteDefinition;
38+
import org.springframework.http.MediaType;
39+
import org.springframework.test.annotation.DirtiesContext;
40+
import org.springframework.test.context.ActiveProfiles;
41+
import org.springframework.test.context.DynamicPropertyRegistry;
42+
import org.springframework.test.context.DynamicPropertySource;
43+
import org.springframework.test.web.reactive.server.WebTestClient;
44+
import org.springframework.web.reactive.function.BodyInserters;
45+
46+
import static org.assertj.core.api.Assertions.assertThat;
47+
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
48+
49+
/**
50+
* @author Peter Müller
51+
*/
52+
@SpringBootTest(properties = {"management.endpoint.gateway.enabled=true",
53+
"management.endpoints.web.exposure.include=*", "spring.cloud.gateway.actuator.verbose.enabled=true"},
54+
webEnvironment = RANDOM_PORT)
55+
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
56+
@ActiveProfiles("redis-route-repository")
57+
@Testcontainers
58+
@Tag("DockerRequired")
59+
public class GatewayControllerEndpointRedisRefreshTest {
60+
61+
@Container
62+
public static GenericContainer redis = new GenericContainer<>("redis:5.0.14-alpine").withExposedPorts(6379);
63+
64+
@BeforeAll
65+
public static void startRedisContainer() {
66+
redis.start();
67+
}
68+
69+
@DynamicPropertySource
70+
static void containerProperties(DynamicPropertyRegistry registry) {
71+
registry.add("spring.data.redis.host", redis::getHost);
72+
registry.add("spring.data.redis.port", redis::getFirstMappedPort);
73+
}
74+
75+
@Autowired
76+
WebTestClient testClient;
77+
78+
@LocalServerPort
79+
int port;
80+
81+
@Test
82+
public void testCorsConfigurationAfterReload() {
83+
Map<String, Object> cors = new HashMap<>();
84+
cors.put("allowCredentials", false);
85+
cors.put("allowedOrigins", "*");
86+
cors.put("allowedMethods", "GET");
87+
88+
createOrUpdateRouteWithCors(cors);
89+
90+
Awaitility.await().atMost(Duration.ofSeconds(3)).untilAsserted(() -> assertRouteHasCorsConfig(cors));
91+
Awaitility.await().atMost(Duration.ofSeconds(3)).untilAsserted(() -> assertPreflightAllowOrigin("*"));
92+
93+
cors.put("allowedOrigins", "http://example.org");
94+
createOrUpdateRouteWithCors(cors);
95+
96+
Awaitility.await().atMost(Duration.ofSeconds(3)).untilAsserted(() -> assertRouteHasCorsConfig(cors));
97+
Awaitility.await().atMost(Duration.ofSeconds(3))
98+
.untilAsserted(() -> assertPreflightAllowOrigin("http://example.org"));
99+
}
100+
101+
void createOrUpdateRouteWithCors(Map<String, Object> cors) {
102+
RouteDefinition testRouteDefinition = new RouteDefinition();
103+
testRouteDefinition.setUri(URI.create("http://example.org"));
104+
105+
PredicateDefinition methodRoutePredicateDefinition = new PredicateDefinition("Method=GET");
106+
testRouteDefinition.setPredicates(List.of(methodRoutePredicateDefinition));
107+
108+
testRouteDefinition.setMetadata(Map.of("cors", cors));
109+
110+
testClient.post()
111+
.uri("http://localhost:" + port + "/actuator/gateway/routes/cors-test-route")
112+
.accept(MediaType.APPLICATION_JSON)
113+
.body(BodyInserters.fromValue(testRouteDefinition))
114+
.exchange()
115+
.expectStatus()
116+
.isCreated();
117+
118+
testClient.post()
119+
.uri("http://localhost:" + port + "/actuator/gateway/refresh")
120+
.exchange()
121+
.expectStatus()
122+
.isOk();
123+
}
124+
125+
void assertRouteHasCorsConfig(Map<String, Object> cors) {
126+
testClient.get()
127+
.uri("http://localhost:" + port + "/actuator/gateway/routes/cors-test-route")
128+
.exchange()
129+
.expectStatus()
130+
.isOk()
131+
.expectBody()
132+
.jsonPath("$.metadata")
133+
.value(map -> assertThat((Map<String, Object>) map).hasSize(1)
134+
.containsEntry("cors", cors));
135+
}
136+
137+
void assertPreflightAllowOrigin(String origin) {
138+
testClient.options()
139+
.uri("http://localhost:" + port + "/")
140+
.header("Origin", "http://example.org")
141+
.header("Access-Control-Request-Method", "GET")
142+
.exchange()
143+
.expectStatus()
144+
.isOk()
145+
.expectHeader()
146+
.valueEquals("Access-Control-Allow-Origin", origin);
147+
}
148+
}

0 commit comments

Comments
 (0)