Merge branch 'master' of https://github.com/spring-cloud/spring-cloud-gateway into metrics-issue-12
This commit is contained in:
@@ -982,6 +982,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
|
||||
|
||||
@@ -285,9 +285,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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -25,7 +25,6 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
@@ -58,7 +57,7 @@ public class ForwardedHeadersFilter implements HttpHeadersFilter, Ordered {
|
||||
List<Forwarded> forwardeds = parse(original.get(FORWARDED_HEADER));
|
||||
|
||||
for (Forwarded f : forwardeds) {
|
||||
updated.add(FORWARDED_HEADER, f.toString());
|
||||
updated.add(FORWARDED_HEADER, f.toHeaderValue());
|
||||
}
|
||||
|
||||
//TODO: add new forwarded
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,23 +17,18 @@
|
||||
|
||||
package org.springframework.cloud.gateway.filter.headers;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.cloud.gateway.filter.headers.ForwardedHeadersFilter.Forwarded;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.*;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.cloud.gateway.filter.headers.ForwardedHeadersFilter.FORWARDED_HEADER;
|
||||
|
||||
@@ -67,6 +62,36 @@ public class ForwardedHeadersFilterTests {
|
||||
.containsEntry("for", "\"10.0.0.1:80\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void forwardedHeaderExists() throws UnknownHostException {
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
.get("http://localhost/get")
|
||||
.remoteAddress(new InetSocketAddress(InetAddress.getByName("10.0.0.1"), 80))
|
||||
.header(FORWARDED_HEADER, "for=12.34.56.78;host=example.com;proto=https; for=23.45.67.89")
|
||||
.build();
|
||||
|
||||
ForwardedHeadersFilter filter = new ForwardedHeadersFilter();
|
||||
|
||||
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
|
||||
|
||||
assertThat(headers.get(FORWARDED_HEADER)).hasSize(2);
|
||||
|
||||
|
||||
List<Forwarded> forwardeds = ForwardedHeadersFilter.parse(headers.get(FORWARDED_HEADER));
|
||||
|
||||
assertThat(forwardeds).hasSize(2);
|
||||
Forwarded addedForwardedHeader = forwardeds.get(0);
|
||||
Forwarded existingForwardedHeader = forwardeds.get(1);
|
||||
|
||||
assertThat(existingForwardedHeader.getValues())
|
||||
.containsEntry("proto", "http")
|
||||
.containsEntry("for", "\"10.0.0.1:80\"");
|
||||
|
||||
assertThat(addedForwardedHeader.getValues())
|
||||
.containsEntry("proto", "https")
|
||||
.containsEntry("for", "23.45.67.89");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noHostHeader() throws UnknownHostException {
|
||||
MockServerHttpRequest request = MockServerHttpRequest
|
||||
@@ -92,7 +117,7 @@ public class ForwardedHeadersFilterTests {
|
||||
|
||||
@Test
|
||||
public void forwardedParsedCorrectly() {
|
||||
String[] valid = new String[] {
|
||||
String[] valid = new String[]{
|
||||
"for=\"_gazonk\"",
|
||||
"for=192.0.2.60;proto=http;by=203.0.113.43",
|
||||
"for=192.0.2.43, for=198.51.100.17",
|
||||
@@ -103,15 +128,15 @@ public class ForwardedHeadersFilterTests {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<List<Map<String, String>>> expectedFor = new ArrayList<List<Map<String, String>>>() {{
|
||||
add(list(map("for", "\"_gazonk\"")));
|
||||
add(list(map("for", "192.0.2.60", "proto", "http", "by", "203.0.113.43")));
|
||||
add(list(map("for", "192.0.2.43"), map("for", "198.51.100.17")));
|
||||
add(list(map("for", "12.34.56.78", "host", "example.com", "proto", "https"),
|
||||
map("for", "23.45.67.89")));
|
||||
add(list(map("for", "12.34.56.78"),
|
||||
map("for", "23.45.67.89", "secret", "egah2CGj55fSJFs"),
|
||||
map("for", "10.1.2.3")));
|
||||
add(list(map("for", "\"[2001:db8:cafe::17]:4711\"")));
|
||||
add(list(map("for", "\"_gazonk\"")));
|
||||
add(list(map("for", "192.0.2.60", "proto", "http", "by", "203.0.113.43")));
|
||||
add(list(map("for", "192.0.2.43"), map("for", "198.51.100.17")));
|
||||
add(list(map("for", "12.34.56.78", "host", "example.com", "proto", "https"),
|
||||
map("for", "23.45.67.89")));
|
||||
add(list(map("for", "12.34.56.78"),
|
||||
map("for", "23.45.67.89", "secret", "egah2CGj55fSJFs"),
|
||||
map("for", "10.1.2.3")));
|
||||
add(list(map("for", "\"[2001:db8:cafe::17]:4711\"")));
|
||||
}};
|
||||
|
||||
for (int i = 0; i < valid.length; i++) {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user