diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 95a8fda0..3f6897aa 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -620,6 +620,24 @@ spring: For a request path of `/foo/bar`, this will set the path to `/bar` before making the downstream request. Notice the `$\` which is replaced with `$` because of the YAML spec. +=== RewriteResponseHeader GatewayFilter Factory +The RewriteResponseHeader GatewayFilter Factory takes `name`, `regexp`, and `replacement` parameters. It uses Java regular expressions for a flexible way to rewrite the response header value. + +.application.yml +[source,yaml] +---- +spring: + cloud: + gateway: + routes: + - id: rewriteresponseheader_route + uri: http://example.org + filters: + - RewriteResponseHeader=X-Response-Foo, , password=[^&]+, password=*** +---- + +For a header value of `/42?user=ford&password=omg!what&flag=true`, it will be set to `/42?user=ford&password=\***&flag=true` before making the downstream request. Please use `$\` to mean `$` because of the YAML spec. + === SaveSession GatewayFilter Factory The SaveSession GatewayFilter Factory forces a `WebSession::save` operation _before_ forwarding the call downstream. This is of particular use when using something like http://projects.spring.io/spring-session/[Spring Session] with a lazy data store and need to ensure the session state has been saved before making the forwarded call. 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 145970c7..15269253 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 @@ -24,6 +24,7 @@ import java.util.function.Consumer; import com.netflix.hystrix.HystrixObservableCommand; import io.netty.channel.ChannelOption; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import org.springframework.cloud.gateway.filter.factory.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.ipc.netty.http.client.HttpClient; @@ -55,28 +56,6 @@ import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter; import org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter; import org.springframework.cloud.gateway.filter.WebsocketRoutingFilter; 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.GatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.HystrixGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.PrefixPathGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.PreserveHostHeaderGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.RedirectToGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.RemoveRequestHeaderGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.RemoveResponseHeaderGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.RequestHeaderToRequestUriGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.RetryGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.SaveSessionGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.SecureHeadersGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.SecureHeadersProperties; -import org.springframework.cloud.gateway.filter.factory.SetPathGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.SetRequestHeaderGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.SetResponseHeaderGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.SetStatusGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.StripPrefixGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory; import org.springframework.cloud.gateway.filter.headers.ForwardedHeadersFilter; @@ -575,6 +554,11 @@ public class GatewayAutoConfiguration { return new SetResponseHeaderGatewayFilterFactory(); } + @Bean + public RewriteResponseHeaderGatewayFilterFactory rewriteResponseHeaderGatewayFilterFactory() { + return new RewriteResponseHeaderGatewayFilterFactory(); + } + @Bean public SetStatusGatewayFilterFactory setStatusGatewayFilterFactory() { return new SetStatusGatewayFilterFactory(); diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RewriteResponseHeaderGatewayFilterFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RewriteResponseHeaderGatewayFilterFactory.java new file mode 100644 index 00000000..ec7d2d55 --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RewriteResponseHeaderGatewayFilterFactory.java @@ -0,0 +1,87 @@ +/* + * 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.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.util.Arrays; +import java.util.List; + +/** + * @author Vitaliy Pavlyuk + */ +public class RewriteResponseHeaderGatewayFilterFactory extends AbstractGatewayFilterFactory { + + public static final String REGEXP_KEY = "regexp"; + public static final String REPLACEMENT_KEY = "replacement"; + + public RewriteResponseHeaderGatewayFilterFactory() { + super(Config.class); + } + + @Override + public List shortcutFieldOrder() { + return Arrays.asList(NAME_KEY, REGEXP_KEY, REPLACEMENT_KEY); + } + + @Override + public GatewayFilter apply(Config config) { + return (exchange, chain) -> chain.filter(exchange).then(Mono.fromRunnable(() -> { + rewriteHeader(exchange, config); + })); + } + + protected void rewriteHeader(ServerWebExchange exchange, Config config) { + final String name = config.getName(); + final String value = exchange.getResponse().getHeaders().getFirst(name); + if (value == null) { + return; + } + final String newValue = rewrite(value, config.getRegexp(), config.getReplacement()); + exchange.getResponse().getHeaders().set(name, newValue); + } + + String rewrite(String value, String regexp, String replacement) { + return value.replaceAll(regexp, replacement.replace("$\\", "$")); + } + + public static class Config extends AbstractGatewayFilterFactory.NameConfig { + private String regexp; + private String replacement; + + public String getRegexp() { + return regexp; + } + + public Config setRegexp(String regexp) { + this.regexp = regexp; + return this; + } + + public String getReplacement() { + return replacement; + } + + public Config setReplacement(String replacement) { + this.replacement = replacement; + return this; + } + } +} 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 cfa4cc7e..5d718e78 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 @@ -29,33 +29,13 @@ import java.util.stream.Stream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.cloud.gateway.filter.factory.*; import reactor.retry.Repeat; import reactor.retry.Retry; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.OrderedGatewayFilter; -import org.springframework.cloud.gateway.filter.factory.AbstractChangeRequestUriGatewayFilterFactory; -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.HystrixGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.PrefixPathGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.PreserveHostHeaderGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.RedirectToGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.RemoveRequestHeaderGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.RemoveResponseHeaderGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.RequestHeaderToRequestUriGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.RetryGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.SaveSessionGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.SecureHeadersGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.SetPathGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.SetRequestHeaderGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.SetResponseHeaderGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.SetStatusGatewayFilterFactory; -import org.springframework.cloud.gateway.filter.factory.StripPrefixGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.rewrite.RewriteFunction; @@ -475,6 +455,18 @@ public class GatewayFilterSpec extends UriSpec { .apply(c -> c.setName(headerName).setValue(headerValue))); } + /** + * A filter that rewrites a header value on the response before it is returned to the client by the Gateway. + * @param headerName the header name + * @param regex a Java regular expression to match the path against + * @param replacement the replacement for the path + * @return a {@link GatewayFilterSpec} that can be used to apply additional filters + */ + public GatewayFilterSpec rewriteResponseHeader(String headerName, String regex, String replacement) { + return filter(getBean(RewriteResponseHeaderGatewayFilterFactory.class) + .apply(c -> c.setReplacement(replacement).setRegexp(regex).setName(headerName))); + } + /** * A filter that sets the status on the response before it is returned to the client by the Gateway. * @param status the status to set on the response diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/RewriteResponseHeaderGatewayFilterFactoryTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/RewriteResponseHeaderGatewayFilterFactoryTests.java new file mode 100644 index 00000000..ad4dde3e --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/RewriteResponseHeaderGatewayFilterFactoryTests.java @@ -0,0 +1,52 @@ +/* + * 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.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 RewriteResponseHeaderGatewayFilterFactoryTests extends BaseWebClientTests { + + @Test + public void rewriteResponseHeaderFilterWorks() { + testClient.get() + .uri("/headers") + .header("Host", "www.rewriteresponseheader.org") + .exchange() + .expectStatus().isOk() + .expectHeader().valueEquals("X-Request-Foo", "/42?user=ford&password=***&flag=true"); + } + + @EnableAutoConfiguration + @SpringBootConfiguration + @Import(DefaultTestConfig.class) + public static class TestConfig { } + +} diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/AdhocTestSuite.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/AdhocTestSuite.java index a6607146..9571479c 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/AdhocTestSuite.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/AdhocTestSuite.java @@ -25,21 +25,7 @@ import org.junit.runners.Suite; import org.junit.runners.Suite.SuiteClasses; import org.junit.runners.model.Statement; -import org.springframework.cloud.gateway.filter.factory.AddRequestHeaderGatewayFilterFactoryTests; -import org.springframework.cloud.gateway.filter.factory.AddRequestParameterGatewayFilterFactoryTests; -import org.springframework.cloud.gateway.filter.factory.HystrixGatewayFilterFactoryTests; -import org.springframework.cloud.gateway.filter.factory.PreserveHostHeaderGatewayFilterFactoryTests; -import org.springframework.cloud.gateway.filter.factory.RedirectToGatewayFilterFactoryTests; -import org.springframework.cloud.gateway.filter.factory.RemoveRequestHeaderGatewayFilterFactoryTests; -import org.springframework.cloud.gateway.filter.factory.RemoveResponseHeaderGatewayFilterFactoryTests; -import org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactoryTests; -import org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactoryIntegrationTests; -import org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactoryTests; -import org.springframework.cloud.gateway.filter.factory.SecureHeadersGatewayFilterFactoryTests; -import org.springframework.cloud.gateway.filter.factory.SetPathGatewayFilterFactoryIntegrationTests; -import org.springframework.cloud.gateway.filter.factory.SetPathGatewayFilterFactoryTests; -import org.springframework.cloud.gateway.filter.factory.SetResponseHeaderGatewayFilterFactoryTests; -import org.springframework.cloud.gateway.filter.factory.SetStatusGatewayFilterFactoryTests; +import org.springframework.cloud.gateway.filter.factory.*; import org.springframework.cloud.gateway.filter.headers.ForwardedHeadersFilterTests; import org.springframework.cloud.gateway.filter.headers.RemoveHopByHopHeadersFilterTests; import org.springframework.cloud.gateway.filter.headers.XForwardedHeadersFilterTests; @@ -89,6 +75,7 @@ import static org.junit.Assume.assumeThat; SetPathGatewayFilterFactoryIntegrationTests.class, AddRequestParameterGatewayFilterFactoryTests.class, SetResponseHeaderGatewayFilterFactoryTests.class, + RewriteResponseHeaderGatewayFilterFactoryTests.class, PrincipalNameKeyResolverIntegrationTests.class, RedisRateLimiterTests.class, RouteDefinitionRouteLocatorTests.class, diff --git a/spring-cloud-gateway-core/src/test/resources/application.yml b/spring-cloud-gateway-core/src/test/resources/application.yml index 41e8bb5d..1199d271 100644 --- a/spring-cloud-gateway-core/src/test/resources/application.yml +++ b/spring-cloud-gateway-core/src/test/resources/application.yml @@ -65,6 +65,16 @@ spring: filters: - AddResponseHeader=X-Request-Foo, Bar + # ===================================== + - id: rewrite_response_header_test + uri: ${test.uri} + predicates: + - Host=**.rewriteresponseheader.org + - Path=/headers + filters: + - AddResponseHeader=X-Request-Foo, /42?user=ford&password=omg!what&flag=true + - RewriteResponseHeader=X-Request-Foo, password=[^&]+, password=*** + # ===================================== - id: forward_test uri: forward:/localcontroller