diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index b18628b0..392397ed 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -607,6 +607,24 @@ their default values: You can find more information on how Hystrix works with Gateway in the <>. +=== MapRequestHeader GatewayFilter Factory +The MapRequestHeader GatewayFilter Factory takes 'fromHeader' and 'toHeader' parameters. It creates a new named header (toHeader) and the value is extracted out of an existing named header (fromHeader) from the incoming http request. If the input header does not exist then the filter has no impact. If the new named header already exists then it's values will be augmented with the new values. + +.application.yml +[source,yaml] +---- +spring: + cloud: + gateway: + routes: + - id: map_request_header_route + uri: https://example.org + filters: + - MapRequestHeader=Bar, X-Request-Foo +---- + +This will add `X-Request-Foo:` header to the downstream request's with updated values from the incoming http request `Bar` header. + === PrefixPath GatewayFilter Factory The PrefixPath GatewayFilter Factory takes a single `prefix` parameter. @@ -854,7 +872,7 @@ spring: - id: rewritelocationresponseheader_route uri: http://example.org filters: - - RewriteLocationResponseHeader=AS_IN_REQUEST, Location, , + - RewriteLocationResponseHeader=AS_IN_REQUEST, Location, , ---- For example, for a request `POST https://api.example.com/some/object/name`, `Location` response header value `https://object-service.prod.example.net/v2/some/object/id` will be rewritten as `https://api.example.com/some/object/id`. diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index a54d9310..bd26394a 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java @@ -68,6 +68,7 @@ import org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGate import org.springframework.cloud.gateway.filter.factory.FallbackHeadersGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.HystrixGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.MapRequestHeaderGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.PrefixPathGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.PreserveHostHeaderGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.RedirectToGatewayFilterFactory; @@ -398,6 +399,11 @@ public class GatewayAutoConfiguration { return new AddRequestHeaderGatewayFilterFactory(); } + @Bean + public MapRequestHeaderGatewayFilterFactory mapRequestHeaderGatewayFilterFactory() { + return new MapRequestHeaderGatewayFilterFactory(); + } + @Bean public AddRequestParameterGatewayFilterFactory addRequestParameterGatewayFilterFactory() { return new AddRequestParameterGatewayFilterFactory(); diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/MapRequestHeaderGatewayFilterFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/MapRequestHeaderGatewayFilterFactory.java new file mode 100644 index 00000000..ce72cfba --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/MapRequestHeaderGatewayFilterFactory.java @@ -0,0 +1,124 @@ +/* + * 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 + * + * 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.filter.factory; + +import java.util.Arrays; +import java.util.List; + +import reactor.core.publisher.Mono; + +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.core.style.ToStringCreator; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.server.ServerWebExchange; + +import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator; + +/** + * @author Tony Clarke + */ +public class MapRequestHeaderGatewayFilterFactory extends + AbstractGatewayFilterFactory { + + /** + * From Header key. + */ + public static final String FROM_HEADER_KEY = "fromHeader"; + + /** + * To Header key. + */ + public static final String TO_HEADER_KEY = "toHeader"; + + public MapRequestHeaderGatewayFilterFactory() { + super(Config.class); + } + + public List shortcutFieldOrder() { + return Arrays.asList(FROM_HEADER_KEY, TO_HEADER_KEY); + } + + @Override + public GatewayFilter apply(MapRequestHeaderGatewayFilterFactory.Config config) { + return new GatewayFilter() { + @Override + public Mono filter(ServerWebExchange exchange, + GatewayFilterChain chain) { + if (!exchange.getRequest().getHeaders() + .containsKey(config.getFromHeader())) { + return chain.filter(exchange); + } + List headerValues = exchange.getRequest().getHeaders() + .get(config.getFromHeader()); + + ServerHttpRequest request = exchange.getRequest().mutate() + .headers(i -> i.addAll(config.getToHeader(), headerValues)) + .build(); + + return chain.filter(exchange.mutate().request(request).build()); + } + + @Override + public String toString() { + // @formatter:off + return filterToStringCreator(MapRequestHeaderGatewayFilterFactory.this) + .append(FROM_HEADER_KEY, config.getFromHeader()) + .append(TO_HEADER_KEY, config.getToHeader()) + .toString(); + // @formatter:on + } + }; + } + + public static class Config { + + private String fromHeader; + + private String toHeader; + + public String getFromHeader() { + return this.fromHeader; + } + + public Config setFromHeader(String fromHeader) { + this.fromHeader = fromHeader; + return this; + } + + public String getToHeader() { + return this.toHeader; + } + + public Config setToHeader(String toHeader) { + this.toHeader = toHeader; + return this; + } + + @Override + public String toString() { + // @formatter:off + return new ToStringCreator(this) + .append("fromHeader", fromHeader) + .append("toHeader", toHeader) + .toString(); + // @formatter:on + } + + } + +} diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java index 828ea441..d76e628d 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java @@ -43,6 +43,7 @@ import org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGate 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.MapRequestHeaderGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.PrefixPathGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.PreserveHostHeaderGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.RedirectToGatewayFilterFactory; @@ -217,6 +218,17 @@ public class GatewayFilterSpec extends UriSpec { return filter(factory.apply(this.routeBuilder.getId(), configConsumer)); } + /** + * Maps headers from one name to another. + * @param fromHeader the header name of the original header. + * @param toHeader the header name of the new header. + * @return a {@link GatewayFilterSpec} that can be used to apply additional filters + */ + public GatewayFilterSpec mapRequestHeader(String fromHeader, String toHeader) { + return filter(getBean(MapRequestHeaderGatewayFilterFactory.class) + .apply(c -> c.setFromHeader(fromHeader).setToHeader(toHeader))); + } + /** * A filter that can be used to modify the request body. This filter is BETA and may * be subject to change in a future release. diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/MapRequestHeaderGatewayFilterFactoryTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/MapRequestHeaderGatewayFilterFactoryTests.java new file mode 100644 index 00000000..9d57bc07 --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/MapRequestHeaderGatewayFilterFactoryTests.java @@ -0,0 +1,138 @@ +/* + * 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 + * + * 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.filter.factory; + +import java.util.List; +import java.util.Map; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.factory.MapRequestHeaderGatewayFilterFactory.Config; +import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; +import org.springframework.cloud.gateway.test.BaseWebClientTests; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; +import static org.springframework.cloud.gateway.test.TestUtils.getMap; + +/** + * @author Tony Clarke + */ +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = RANDOM_PORT) +@DirtiesContext +@ActiveProfiles(profiles = "request-map-header-web-filter") +public class MapRequestHeaderGatewayFilterFactoryTests extends BaseWebClientTests { + + @Test + public void mapRequestHeaderFilterWorks() { + testClient.get().uri("/headers").header("Host", "www.maprequestheader.org") + .header("a", "tome").exchange().expectBody(Map.class) + .consumeWith(result -> { + Map headers = getMap(result.getResponseBody(), + "headers"); + assertThat(headers).containsEntry("X-Request-Example", "tome"); + }); + } + + @Test + public void mapRequestHeaderFilterWorksJavaDsl() { + testClient.get().uri("/headers").header("Host", "www.maprequestheaderjava.org") + .header("b", "tome").exchange().expectBody(Map.class) + .consumeWith(result -> { + Map headers = getMap(result.getResponseBody(), + "headers"); + assertThat(headers).containsEntry("X-Request-Example-Java", "tome"); + }); + } + + @SuppressWarnings("unchecked") + @Test + public void mapRequestHeaderWithMultiValueFilterWorks() { + testClient.get().uri("/multivalueheaders") + .header("Host", "www.maprequestheader.org").header("a", "tome", "toyou") + .exchange().expectBody(Map.class).consumeWith(result -> { + Map headers = getMap(result.getResponseBody(), + "headers"); + assertThat(headers).containsKey("X-Request-Example"); + List values = (List) headers.get("X-Request-Example"); + assertThat(values).contains("tome", "toyou"); + }); + } + + @Test + public void mapRequestHeaderWithNullValueFilterWorks() { + testClient.get().uri("/headers").header("Host", "www.maprequestheader.org") + .header("a", (String) null).exchange().expectBody(Map.class) + .consumeWith(result -> { + Map headers = getMap(result.getResponseBody(), + "headers"); + assertThat(headers).doesNotContainKey("X-Request-Example"); + }); + } + + @Test + public void mapRequestHeaderWhenInputHeaderDoesNotExist() { + testClient.get().uri("/headers").header("Host", "www.maprequestheader.org") + .exchange().expectBody(Map.class).consumeWith(result -> { + Map headers = getMap(result.getResponseBody(), + "headers"); + assertThat(headers).doesNotContainKey("X-Request-Example"); + }); + } + + @Test + public void toStringFormat() { + Config config = new Config().setFromHeader("myfromheader") + .setToHeader("mytoheader"); + GatewayFilter filter = new MapRequestHeaderGatewayFilterFactory().apply(config); + assertThat(filter.toString()).contains("myfromheader").contains("mytoheader"); + } + + @EnableAutoConfiguration + @SpringBootConfiguration + @Import(DefaultTestConfig.class) + public static class TestConfig { + + @Value("${test.uri}") + String uri; + + @Bean + public RouteLocator testRouteLocator(RouteLocatorBuilder builder) { + return builder.routes().route("map_request_header_java_test", + r -> r.path("/headers").and().host("**.maprequestheaderjava.org") + .filters(f -> f.prefixPath("/httpbin").mapRequestHeader("b", + "X-Request-Example-Java")) + .uri(uri)) + .build(); + } + + } + +} diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/HttpBinCompatibleController.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/HttpBinCompatibleController.java index 6c10969c..695906b1 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/HttpBinCompatibleController.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/HttpBinCompatibleController.java @@ -59,6 +59,14 @@ public class HttpBinCompatibleController { return result; } + @RequestMapping(path = "/multivalueheaders", method = { RequestMethod.GET, + RequestMethod.POST }, produces = MediaType.APPLICATION_JSON_VALUE) + public Map multiValueHeaders(ServerWebExchange exchange) { + Map result = new HashMap<>(); + result.put("headers", exchange.getRequest().getHeaders()); + return result; + } + @RequestMapping(path = "/delay/{sec}", produces = MediaType.APPLICATION_JSON_VALUE) public Mono> get(ServerWebExchange exchange, @PathVariable int sec) throws InterruptedException { diff --git a/spring-cloud-gateway-core/src/test/resources/application-request-map-header-web-filter.yml b/spring-cloud-gateway-core/src/test/resources/application-request-map-header-web-filter.yml new file mode 100644 index 00000000..fefe27db --- /dev/null +++ b/spring-cloud-gateway-core/src/test/resources/application-request-map-header-web-filter.yml @@ -0,0 +1,16 @@ +spring: + cloud: + gateway: + routes: + - id: map_request_header_test_singleresponse + uri: ${test.uri} + predicates: + - Path=/headers + filters: + - MapRequestHeader=a, X-Request-Example + - id: map_request_header_test_multiresponse + uri: ${test.uri} + predicates: + - Path=/multivalueheaders + filters: + - MapRequestHeader=a, X-Request-Example