Browse Source

Merge branch '2.1.x'

pull/927/head
Spencer Gibb 6 years ago
parent
commit
1f231df13c
No known key found for this signature in database
GPG Key ID: 7788A47380690861
  1. 2
      docs/pom.xml
  2. 22
      docs/src/main/asciidoc/spring-cloud-gateway.adoc
  3. 6
      spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java
  4. 154
      spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/DedupeResponseHeaderGatewayFilterFactory.java
  5. 2
      spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java
  6. 14
      spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java
  7. 54
      spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/DedupeResponseHeaderGatewayFilterFactoryTests.java
  8. 123
      spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/DedupeResponseHeaderGatewayFilterFactoryUnitTests.java
  9. 10
      spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactoryTests.java
  10. 21
      spring-cloud-gateway-core/src/test/resources/application.yml

2
docs/pom.xml

@ -15,7 +15,7 @@ @@ -15,7 +15,7 @@
<properties>
<docs.main>spring-cloud-gateway</docs.main>
<main.basedir>${basedir}/..</main.basedir>
<docs.whitelisted.branches>1.0.x</docs.whitelisted.branches>
<docs.whitelisted.branches>1.0.x,2.1.x</docs.whitelisted.branches>
</properties>
<build>
<plugins>

22
docs/src/main/asciidoc/spring-cloud-gateway.adoc

@ -369,7 +369,7 @@ spring: @@ -369,7 +369,7 @@ spring:
cloud:
gateway:
routes:
- id: add_request_header_route
- id: add_response_header_route
uri: http://example.org
filters:
- AddResponseHeader=X-Response-Foo, Bar
@ -377,6 +377,26 @@ spring: @@ -377,6 +377,26 @@ spring:
This will add `X-Response-Foo:Bar` header to the downstream response's headers for all matching requests.
=== DedupeResponseHeader GatewayFilter Factory
The DedupeResponseHeader GatewayFilter Factory takes a `name` parameter and an optional `strategy` parameter. `name` can contain a list of header names, space separated.
.application.yml
[source,yaml]
----
spring:
cloud:
gateway:
routes:
- id: dedupe_response_header_route
uri: http://example.org
filters:
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
----
This will remove duplicate values of `Access-Control-Allow-Credentials` and `Access-Control-Allow-Origin` response headers in cases when both the gateway CORS logic and the downstream add them.
The DedupeResponseHeader filter also accepts an optional `strategy` parameter. The accepted values are `RETAIN_FIRST` (default), `RETAIN_LAST`, and `RETAIN_UNIQUE`.
[[hystrix]]
=== Hystrix GatewayFilter Factory
https://github.com/Netflix/Hystrix[Hystrix] is a library from Netflix that implements the https://martinfowler.com/bliki/CircuitBreaker.html[circuit breaker pattern].

6
spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java

@ -56,6 +56,7 @@ import org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter; @@ -56,6 +56,7 @@ import org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter;
import org.springframework.cloud.gateway.filter.factory.AddRequestHeaderGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.AddRequestParameterGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.AddResponseHeaderGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.FallbackHeadersGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.HystrixGatewayFilterFactory;
@ -395,6 +396,11 @@ public class GatewayAutoConfiguration { @@ -395,6 +396,11 @@ public class GatewayAutoConfiguration {
return new ModifyRequestBodyGatewayFilterFactory(codecConfigurer);
}
@Bean
public DedupeResponseHeaderGatewayFilterFactory dedupeResponseHeaderGatewayFilterFactory() {
return new DedupeResponseHeaderGatewayFilterFactory();
}
@Bean
public ModifyResponseBodyGatewayFilterFactory modifyResponseBodyGatewayFilterFactory(
ServerCodecConfigurer codecConfigurer) {

154
spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/DedupeResponseHeaderGatewayFilterFactory.java

@ -0,0 +1,154 @@ @@ -0,0 +1,154 @@
/*
* Copyright 2013-2019 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.filter.factory;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import reactor.core.publisher.Mono;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.http.HttpHeaders;
/*
Use case: Both your legacy backend and your API gateway add CORS header values. So, your consumer ends up with
Access-Control-Allow-Credentials: true, true
Access-Control-Allow-Origin: https://musk.mars, https://musk.mars
(The one from the gateway will be the first of the two.) To fix, add
DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
Configuration parameters:
- name
String representing response header names, space separated. Required.
- strategy
RETAIN_FIRST - Default. Retain the first value only.
RETAIN_LAST - Retain the last value only.
RETAIN_UNIQUE - Retain all unique values in the order of their first encounter.
Example 1
default-filters:
- DedupeResponseHeader=Access-Control-Allow-Credentials
Response header Access-Control-Allow-Credentials: true, false
Modified response header Access-Control-Allow-Credentials: true
Example 2
default-filters:
- DedupeResponseHeader=Access-Control-Allow-Credentials, RETAIN_LAST
Response header Access-Control-Allow-Credentials: true, false
Modified response header Access-Control-Allow-Credentials: false
Example 3
default-filters:
- DedupeResponseHeader=Access-Control-Allow-Credentials, RETAIN_UNIQUE
Response header Access-Control-Allow-Credentials: true, true
Modified response header Access-Control-Allow-Credentials: true
*/
/**
* @author Vitaliy Pavlyuk
*/
public class DedupeResponseHeaderGatewayFilterFactory extends
AbstractGatewayFilterFactory<DedupeResponseHeaderGatewayFilterFactory.Config> {
private static final String STRATEGY_KEY = "strategy";
public DedupeResponseHeaderGatewayFilterFactory() {
super(Config.class);
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList(NAME_KEY, STRATEGY_KEY);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> chain.filter(exchange).then(Mono.fromRunnable(() -> {
dedupe(exchange.getResponse().getHeaders(), config);
}));
}
public enum Strategy {
/**
* Default: Retain the first value only.
*/
RETAIN_FIRST,
/**
* Retain the last value only.
*/
RETAIN_LAST,
/**
* Retain all unique values in the order of their first encounter.
*/
RETAIN_UNIQUE
}
void dedupe(HttpHeaders headers, Config config) {
String names = config.getName();
Strategy strategy = config.getStrategy();
if (headers == null || names == null || strategy == null) {
return;
}
for (String name : names.split(" ")) {
dedupe(headers, name.trim(), strategy);
}
}
private void dedupe(HttpHeaders headers, String name, Strategy strategy) {
List<String> values = headers.get(name);
if (values == null || values.size() <= 1) {
return;
}
switch (strategy) {
case RETAIN_FIRST:
headers.set(name, values.get(0));
break;
case RETAIN_LAST:
headers.set(name, values.get(values.size() - 1));
break;
case RETAIN_UNIQUE:
headers.put(name, values.stream().distinct().collect(Collectors.toList()));
break;
default:
break;
}
}
public static class Config extends AbstractGatewayFilterFactory.NameConfig {
private Strategy strategy = Strategy.RETAIN_FIRST;
public Strategy getStrategy() {
return strategy;
}
public Config setStrategy(Strategy strategy) {
this.strategy = strategy;
return this;
}
}
}

2
spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java

@ -88,7 +88,7 @@ public class PathRoutePredicateFactory @@ -88,7 +88,7 @@ public class PathRoutePredicateFactory
});
}
return exchange -> {
PathContainer path = parsePath(exchange.getRequest().getURI().getPath());
PathContainer path = parsePath(exchange.getRequest().getURI().getRawPath());
Optional<PathPattern> optionalPathPattern = pathPatterns.stream()
.filter(pattern -> pattern.matches(path)).findFirst();

14
spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java

@ -39,6 +39,8 @@ import org.springframework.cloud.gateway.filter.factory.AbstractChangeRequestUri @@ -39,6 +39,8 @@ import org.springframework.cloud.gateway.filter.factory.AbstractChangeRequestUri
import org.springframework.cloud.gateway.filter.factory.AddRequestHeaderGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.AddRequestParameterGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.AddResponseHeaderGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory.Strategy;
import org.springframework.cloud.gateway.filter.factory.FallbackHeadersGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.HystrixGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.PrefixPathGatewayFilterFactory;
@ -177,6 +179,18 @@ public class GatewayFilterSpec extends UriSpec { @@ -177,6 +179,18 @@ public class GatewayFilterSpec extends UriSpec {
.apply(c -> c.setName(headerName).setValue(headerValue)));
}
/**
* A filter that removes duplication on a response header before it is returned to the
* client by the Gateway.
* @param headerName the header name(s), space separated
* @param strategy RETAIN_FIRST, RETAIN_LAST, or RETAIN_UNIQUE
* @return a {@link GatewayFilterSpec} that can be used to apply additional filters
*/
public GatewayFilterSpec dedupeResponseHeader(String headerName, String strategy) {
return filter(getBean(DedupeResponseHeaderGatewayFilterFactory.class).apply(
c -> c.setStrategy(Strategy.valueOf(strategy)).setName(headerName)));
}
/**
* Wraps the route in a Hystrix command. Depends on @{code
* org.springframework.cloud::spring-cloud-starter-netflix-hystrix} being on the

54
spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/DedupeResponseHeaderGatewayFilterFactoryTests.java

@ -0,0 +1,54 @@ @@ -0,0 +1,54 @@
/*
* 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.filter.factory;
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.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
@DirtiesContext
public class DedupeResponseHeaderGatewayFilterFactoryTests extends BaseWebClientTests {
@Test
public void dedupeResponseHeaderFilterWorks() {
testClient.get().uri("/headers").header("Host", "www.deduperesponseheader.org")
.exchange().expectStatus().isOk().expectHeader()
.valueEquals("Access-Control-Allow-Credentials", "true").expectHeader()
.valueEquals("Access-Control-Allow-Origin", "https://musk.mars")
.expectHeader().valueEquals("Scout-Cookie", "S'mores").expectHeader()
.valueEquals("Next-Week-Lottery-Numbers", "4", "2", "42");
}
@EnableAutoConfiguration
@SpringBootConfiguration
@Import(DefaultTestConfig.class)
public static class TestConfig {
}
}

123
spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/DedupeResponseHeaderGatewayFilterFactoryUnitTests.java

@ -0,0 +1,123 @@ @@ -0,0 +1,123 @@
/*
* Copyright 2013-2019 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.filter.factory;
import java.util.ArrayList;
import java.util.Arrays;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.http.HttpHeaders;
public class DedupeResponseHeaderGatewayFilterFactoryUnitTests {
private static final String NAME_1 = HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN;
private static final String NAME_2 = HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS;
private HttpHeaders headers;
private DedupeResponseHeaderGatewayFilterFactory.Config config;
private DedupeResponseHeaderGatewayFilterFactory filter;
@Before
public void setUp() {
headers = Mockito.mock(HttpHeaders.class);
config = new DedupeResponseHeaderGatewayFilterFactory.Config();
filter = new DedupeResponseHeaderGatewayFilterFactory();
}
@Test
public void dedupNullName() {
filter.dedupe(headers, config);
Mockito.verify(headers, Mockito.never()).get(Mockito.anyString());
Mockito.verify(headers, Mockito.never()).set(Mockito.anyString(),
Mockito.anyString());
}
@Test
public void dedupNullValues() {
config.setName(NAME_1);
Mockito.when(headers.get(NAME_1)).thenReturn(null);
filter.dedupe(headers, config);
Mockito.verify(headers).get(NAME_1);
Mockito.verify(headers, Mockito.never()).set(Mockito.anyString(),
Mockito.anyString());
}
@Test
public void dedupEmptyValues() {
config.setName(NAME_1);
Mockito.when(headers.get(NAME_1)).thenReturn(new ArrayList<>());
filter.dedupe(headers, config);
Mockito.verify(headers).get(NAME_1);
Mockito.verify(headers, Mockito.never()).set(Mockito.anyString(),
Mockito.anyString());
}
@Test
public void dedupSingleValue() {
config.setName(NAME_1);
Mockito.when(headers.get(NAME_1)).thenReturn(Arrays.asList("1"));
filter.dedupe(headers, config);
Mockito.verify(headers).get(NAME_1);
Mockito.verify(headers, Mockito.never()).set(Mockito.anyString(),
Mockito.anyString());
}
@Test
public void dedupMultipleValuesRetainFirst() {
config.setName(NAME_1 + " " + NAME_2);
Mockito.when(headers.get(NAME_1)).thenReturn(Arrays.asList("2", "3", "3", "4"));
Mockito.when(headers.get(NAME_2)).thenReturn(Arrays.asList("true", "false"));
filter.dedupe(headers, config);
Mockito.verify(headers).get(NAME_1);
Mockito.verify(headers).set(NAME_1, "2");
Mockito.verify(headers).get(NAME_2);
Mockito.verify(headers).set(NAME_2, "true");
Mockito.verify(headers, Mockito.times(2)).set(Mockito.anyString(),
Mockito.anyString());
}
@Test
public void dedupMultipleValuesRetainLast() {
config.setName(NAME_1);
config.setStrategy(DedupeResponseHeaderGatewayFilterFactory.Strategy.RETAIN_LAST);
Mockito.when(headers.get(NAME_1)).thenReturn(Arrays.asList("2", "3", "3", "4"));
filter.dedupe(headers, config);
Mockito.verify(headers).get(NAME_1);
Mockito.verify(headers).set(NAME_1, "4");
Mockito.verify(headers).set(Mockito.anyString(), Mockito.anyString());
}
@Test
public void dedupMultipleValuesRetainUnique() {
config.setName(NAME_1);
config.setStrategy(
DedupeResponseHeaderGatewayFilterFactory.Strategy.RETAIN_UNIQUE);
Mockito.when(headers.get(NAME_1)).thenReturn(Arrays.asList("2", "3", "3", "4"));
filter.dedupe(headers, config);
Mockito.verify(headers).get(NAME_1);
Mockito.verify(headers).put(Mockito.eq(NAME_1),
Mockito.eq(Arrays.asList("2", "3", "4")));
Mockito.verify(headers).put(Mockito.anyString(), Mockito.anyList());
}
}

10
spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactoryTests.java

@ -83,6 +83,16 @@ public class PathRoutePredicateFactoryTests extends BaseWebClientTests { @@ -83,6 +83,16 @@ public class PathRoutePredicateFactoryTests extends BaseWebClientTests {
expectPathRoute("/anything/multidsl3", "www.pathmultidsl.org", "path_multi_dsl");
}
@Test
public void pathRouteWorksWithPercent() {
testClient.get().uri("/abc/123%/function")
.header(HttpHeaders.HOST, "www.path.org").exchange().expectStatus().isOk()
.expectHeader()
.valueEquals(HANDLER_MAPPER_HEADER,
RoutePredicateHandlerMapping.class.getSimpleName())
.expectHeader().valueEquals(ROUTE_ID_HEADER, "path_test");
}
@EnableAutoConfiguration
@SpringBootConfiguration
@Import(DefaultTestConfig.class)

21
spring-cloud-gateway-core/src/test/resources/application.yml

@ -69,6 +69,27 @@ spring: @@ -69,6 +69,27 @@ spring:
- AddResponseHeader=X-Request-Foo, Bar
# =====================================
- id: dedupe_response_header_test
uri: ${test.uri}
predicates:
- Host=**.deduperesponseheader.org
- Path=/headers
filters:
- AddResponseHeader=Access-Control-Allow-Credentials, true
- AddResponseHeader=Access-Control-Allow-Credentials, false
- AddResponseHeader=Access-Control-Allow-Origin, https://musk.mars
- AddResponseHeader=Access-Control-Allow-Origin, *
- AddResponseHeader=Scout-Cookie, Thin Mints
- AddResponseHeader=Scout-Cookie, S'mores
- AddResponseHeader=Next-Week-Lottery-Numbers, 4
- AddResponseHeader=Next-Week-Lottery-Numbers, 2
- AddResponseHeader=Next-Week-Lottery-Numbers, 2
- AddResponseHeader=Next-Week-Lottery-Numbers, 42
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin, RETAIN_FIRST
- DedupeResponseHeader=Scout-Cookie, RETAIN_LAST
- DedupeResponseHeader=Next-Week-Lottery-Numbers, RETAIN_UNIQUE
# =====================================
- id: rewrite_response_header_test
uri: ${test.uri}
predicates:

Loading…
Cancel
Save