Merge pull request #401 from tony-clarke-amdocs/master

Support CORS configuration.  Fixes #229
This commit is contained in:
Ryan Baxter
2018-07-20 15:57:55 -04:00
committed by GitHub
7 changed files with 178 additions and 11 deletions

View File

@@ -971,6 +971,26 @@ The Gateway can be configured to create routes based on services registered with
To enable this, set `spring.cloud.gateway.discovery.locator.enabled=true` and make sure a `DiscoveryClient` implementation is on the classpath and enabled (such as Netflix Eureka, Consul or Zookeeper).
== CORS Configuration
The gateway can be configured to control CORS behavior. The "global" CORS configuration is a map of URL patterns to https://docs.spring.io/spring/docs/5.0.x/javadoc-api/org/springframework/web/cors/CorsConfiguration.html[Spring Framework `CorsConfiguration`].
.application.yml
[source,yaml]
----
spring:
cloud:
gateway:
globalcors:
corsConfigurations:
'[/**]':
allowedOrigins: "docs.spring.io"
allowedMethods:
- GET
----
In the example above, CORS requests will be allowed from requests that originate from docs.spring.io for all GET requested paths.
== Actuator API
TODO: document the `/gateway` actuator endpoint

View File

@@ -282,9 +282,14 @@ public class GatewayAutoConfiguration {
}
@Bean
public RoutePredicateHandlerMapping routePredicateHandlerMapping(FilteringWebHandler webHandler,
RouteLocator routeLocator) {
return new RoutePredicateHandlerMapping(webHandler, routeLocator);
public GlobalCorsProperties globalCorsProperties() {
return new GlobalCorsProperties();
}
@Bean
public RoutePredicateHandlerMapping routePredicateHandlerMapping(FilteringWebHandler webHandler, RouteLocator routeLocator,
GlobalCorsProperties globalCorsProperties) {
return new RoutePredicateHandlerMapping(webHandler, routeLocator, globalCorsProperties);
}
// ConfigurationProperty beans

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2013-2018 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
*
* http://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.config;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping;
import org.springframework.web.cors.CorsConfiguration;
/**
* Configuration properties for global configuration of cors. See
* {@link RoutePredicateHandlerMapping}
*/
@ConfigurationProperties("spring.cloud.gateway.globalcors")
public class GlobalCorsProperties {
private final Map<String, CorsConfiguration> corsConfigurations = new LinkedHashMap<>();
public Map<String, CorsConfiguration> getCorsConfigurations() {
return corsConfigurations;
}
}

View File

@@ -19,8 +19,8 @@ package org.springframework.cloud.gateway.handler;
import java.util.function.Function;
import org.springframework.cloud.gateway.config.GlobalCorsProperties;
import reactor.core.publisher.Mono;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.web.cors.CorsConfiguration;
@@ -39,11 +39,12 @@ public class RoutePredicateHandlerMapping extends AbstractHandlerMapping {
private final FilteringWebHandler webHandler;
private final RouteLocator routeLocator;
public RoutePredicateHandlerMapping(FilteringWebHandler webHandler, RouteLocator routeLocator) {
public RoutePredicateHandlerMapping(FilteringWebHandler webHandler, RouteLocator routeLocator, GlobalCorsProperties globalCorsProperties) {
this.webHandler = webHandler;
this.routeLocator = routeLocator;
setOrder(1);
setOrder(1);
setCorsConfigurations(globalCorsProperties.getCorsConfigurations());
}
@Override
@@ -70,10 +71,10 @@ public class RoutePredicateHandlerMapping extends AbstractHandlerMapping {
@Override
protected CorsConfiguration getCorsConfiguration(Object handler, ServerWebExchange exchange) {
//TODO: support cors configuration via global properties and
// properties on a route see gh-229
// TODO: support cors configuration via properties on a route see gh-229
// see RequestMappingHandlerMapping.initCorsConfiguration()
// also see https://github.com/spring-projects/spring-framework/blob/master/spring-web/src/test/java/org/springframework/web/cors/reactive/CorsWebFilterTests.java
// also see https://github.com/spring-projects/spring-framework/blob/master/spring-web/src/test/java/org/springframework/web/cors/reactive/CorsWebFilterTests.java
return super.getCorsConfiguration(handler, exchange);
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2013-2017 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
*
* http://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.cors;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
import java.util.Arrays;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.gateway.test.BaseWebClientTests;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.reactive.function.client.ClientResponse;
import reactor.core.publisher.Mono;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
@DirtiesContext
public class CorsTests extends BaseWebClientTests {
@Test
public void testPreFlightCorsRequest() {
ClientResponse clientResponse = webClient.options().uri("/abc/123/function")
.header("Origin", "domain.com")
.header("Access-Control-Request-Method", "GET").exchange().block();
HttpHeaders asHttpHeaders = clientResponse.headers().asHttpHeaders();
Mono<String> bodyToMono = clientResponse.bodyToMono(String.class);
// pre-flight request shouldn't return the response body
assertNull(bodyToMono.block());
assertEquals(
"Missing header value in response: "
+ HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN,
"*", asHttpHeaders.getAccessControlAllowOrigin());
assertEquals(
"Missing header value in response: "
+ HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS,
Arrays.asList(new HttpMethod[] { HttpMethod.GET, HttpMethod.HEAD }),
asHttpHeaders.getAccessControlAllowMethods());
assertEquals("Pre Flight call failed.", HttpStatus.OK,
clientResponse.statusCode());
}
@Test
public void testCorsRequest() {
ClientResponse clientResponse = webClient.get().uri("/abc/123/function")
.header("Origin", "domain.com").header(HttpHeaders.HOST, "www.path.org")
.exchange().block();
HttpHeaders asHttpHeaders = clientResponse.headers().asHttpHeaders();
Mono<String> bodyToMono = clientResponse.bodyToMono(String.class);
assertNotNull(bodyToMono.block());
assertEquals(
"Missing header value in response: "
+ HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN,
"*", asHttpHeaders.getAccessControlAllowOrigin());
assertEquals("CORS request failed.", HttpStatus.OK,
clientResponse.statusCode());
}
@EnableAutoConfiguration
@SpringBootConfiguration
@Import(DefaultTestConfig.class)
public static class TestConfig {
}
}

View File

@@ -4,6 +4,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.boot.test.rule.OutputCapture;
import org.springframework.cloud.gateway.config.GlobalCorsProperties;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.web.server.ServerWebExchange;
@@ -41,7 +42,7 @@ public class RoutePredicateHandlerMappingTest {
RouteLocator routeLocator =
() -> Flux.just(routeFalse, routeFail, routeTrue).hide();
RoutePredicateHandlerMapping mapping =
new RoutePredicateHandlerMapping(null, routeLocator);
new RoutePredicateHandlerMapping(null, routeLocator, new GlobalCorsProperties());
final Mono<Route> routeMono =
mapping.lookupRoute(Mockito.mock(ServerWebExchange.class));
@@ -79,7 +80,7 @@ public class RoutePredicateHandlerMappingTest {
RouteLocator routeLocator =
() -> Flux.just(routeFalse, routeError, routeFail, routeTrue).hide();
RoutePredicateHandlerMapping mapping =
new RoutePredicateHandlerMapping(null, routeLocator);
new RoutePredicateHandlerMapping(null, routeLocator, new GlobalCorsProperties());
final Mono<Route> routeMono =
mapping.lookupRoute(Mockito.mock(ServerWebExchange.class));

View File

@@ -7,6 +7,13 @@ test:
spring:
cloud:
gateway:
globalcors:
corsConfigurations:
'[/**]':
maxAge: 10
allowedOrigins: "*"
allowedMethods:
- GET
default-filters:
- AddResponseHeader=X-Response-Default-Foo, Default-Bar
- PrefixPath=/httpbin