From c6ed8455579fd64ec15b9f56da6ec38f029531cb Mon Sep 17 00:00:00 2001 From: Stepan Mikhailiuk Date: Sun, 13 Apr 2025 22:11:33 +0800 Subject: [PATCH] feat(gateway-filter): add SetRequestUri gateway filter Signed-off-by: Stepan Mikhailiuk --- .../seturi-factory.adoc | 26 ++++ .../config/GatewayAutoConfiguration.java | 7 ++ .../SetRequestUriGatewayFilterFactory.java | 112 ++++++++++++++++++ .../route/builder/GatewayFilterSpec.java | 11 ++ ...iGatewayFilterFactoryIntegrationTests.java | 79 ++++++++++++ ...etRequestUriGatewayFilterFactoryTests.java | 83 +++++++++++++ .../cloud/gateway/test/AdhocTestSuite.java | 2 + 7 files changed, 320 insertions(+) create mode 100644 docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/seturi-factory.adoc create mode 100644 spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/SetRequestUriGatewayFilterFactory.java create mode 100644 spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/SetRequestUriGatewayFilterFactoryIntegrationTests.java create mode 100644 spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/SetRequestUriGatewayFilterFactoryTests.java diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/seturi-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/seturi-factory.adoc new file mode 100644 index 00000000..2251d13b --- /dev/null +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/seturi-factory.adoc @@ -0,0 +1,26 @@ +[[seturi-gatewayfilter-factory]] += `SetRequestUri` `GatewayFilter` Factory + +The `SetRequestUri` `GatewayFilter` factory takes a `uri` parameter. +It offers a simple way to manipulate the request uri by allowing templated segments of the path. +This uses the URI templates from Spring Framework. +Multiple matching segments are allowed. +The following listing configures a `SetRequestUri` `GatewayFilter`: + +.application.yml +[source,yaml] +---- +spring: + cloud: + gateway: + routes: + - id: seturi_route + uri: no://op + predicates: + - Path=/{appId}/** + filters: + - SetRequestUri=http://{appId}.example.com +---- + +For a request path of `/red-application/blue`, this sets the uri to `http://red-application.example.com` before making the downstream request and the final url, including path is going to be `http://red-application.example.com/red-application/blue` + diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index 5d399d95..60108fa6 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java @@ -110,6 +110,7 @@ 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.SetRequestHostHeaderGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.SetRequestUriGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.SetResponseHeaderGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.SetStatusGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.StripPrefixGatewayFilterFactory; @@ -718,6 +719,12 @@ public class GatewayAutoConfiguration { return new RequestHeaderToRequestUriGatewayFilterFactory(); } + @Bean + @ConditionalOnEnabledFilter + public SetRequestUriGatewayFilterFactory setRequestUriGatewayFilterFactory() { + return new SetRequestUriGatewayFilterFactory(); + } + @Bean @ConditionalOnEnabledFilter public RequestSizeGatewayFilterFactory requestSizeGatewayFilterFactory() { diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/SetRequestUriGatewayFilterFactory.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/SetRequestUriGatewayFilterFactory.java new file mode 100644 index 00000000..2910d88d --- /dev/null +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/SetRequestUriGatewayFilterFactory.java @@ -0,0 +1,112 @@ +/* + * Copyright 2013-2020 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.net.URI; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.OrderedGatewayFilter; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.util.UriComponentsBuilder; + +import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.getUriTemplateVariables; + +/** + * This filter changes the request uri. + * + * @author Stepan Mikhailiuk + */ +public class SetRequestUriGatewayFilterFactory + extends AbstractChangeRequestUriGatewayFilterFactory { + + private static final Logger log = LoggerFactory.getLogger(SetRequestUriGatewayFilterFactory.class); + + public SetRequestUriGatewayFilterFactory() { + super(Config.class); + } + + @Override + public List shortcutFieldOrder() { + return Arrays.asList(NAME_KEY); + } + + @Override + public GatewayFilter apply(Config config) { + // AbstractChangeRequestUriGatewayFilterFactory.apply() returns + // OrderedGatewayFilter + OrderedGatewayFilter gatewayFilter = (OrderedGatewayFilter) super.apply(config); + return new OrderedGatewayFilter(gatewayFilter, gatewayFilter.getOrder()) { + @Override + public String toString() { + return filterToStringCreator(SetRequestUriGatewayFilterFactory.this) + .append("template", config.getTemplate()) + .toString(); + } + }; + } + + String getUri(ServerWebExchange exchange, Config config) { + String template = config.getTemplate(); + + if (template.indexOf('{') == -1) { + return template; + } + + Map variables = getUriTemplateVariables(exchange); + return UriComponentsBuilder.fromUriString(template).build().expand(variables).toUriString(); + } + + @Override + protected Optional determineRequestUri(ServerWebExchange exchange, Config config) { + try { + String url = getUri(exchange, config); + URI uri = URI.create(url); + if (!uri.isAbsolute()) { + throw new IllegalArgumentException("URI is not absolute"); + } + return Optional.of(uri); + } + catch (IllegalArgumentException e) { + + log.info("Request url is invalid : url={}, error={}", config.getTemplate(), e.getMessage()); + return Optional.ofNullable(null); + } + } + + public static class Config { + + private String template; + + public String getTemplate() { + return template; + } + + public void setTemplate(String template) { + this.template = template; + } + + } + +} diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java index 47723b74..8b87e45a 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java @@ -58,6 +58,7 @@ import org.springframework.cloud.gateway.filter.factory.RemoveRequestParameterGa import org.springframework.cloud.gateway.filter.factory.RemoveResponseHeaderGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.RequestHeaderSizeGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.RequestHeaderToRequestUriGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.SetRequestUriGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.RequestSizeGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.RetryGatewayFilterFactory; @@ -869,6 +870,16 @@ public class GatewayFilterSpec extends UriSpec { return filter(getBean(RequestHeaderToRequestUriGatewayFilterFactory.class).apply(c -> c.setName(headerName))); } + /** + * A filter which changes the URI the request will be routed to by the Gateway by + * pulling it from a header on the request. + * @param uri the URI + * @return a {@link GatewayFilterSpec} that can be used to apply additional filters + */ + public GatewayFilterSpec setRequestUri(String uri) { + return filter(getBean(SetRequestUriGatewayFilterFactory.class).apply(c -> c.setTemplate(uri))); + } + /** * A filter which change the URI the request will be routed to by the Gateway. * @param determineRequestUri a {@link Function} which takes a diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/SetRequestUriGatewayFilterFactoryIntegrationTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/SetRequestUriGatewayFilterFactoryIntegrationTests.java new file mode 100644 index 00000000..b52a81af --- /dev/null +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/SetRequestUriGatewayFilterFactoryIntegrationTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2013-2020 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 org.junit.jupiter.api.Test; + +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +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 static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +/** + * @author Stepan Mikhailiuk + */ +@SpringBootTest(webEnvironment = RANDOM_PORT) +@DirtiesContext +public class SetRequestUriGatewayFilterFactoryIntegrationTests extends BaseWebClientTests { + + @LocalServerPort + int port; + + @Test + public void setUriWorkWithProperties() { + testClient.get().uri("/").header("Host", "testservice.setrequesturi.org").exchange().expectStatus().isOk(); + + testClient.get() + .uri("/service/testservice") + .header("Host", "setrequesturi.org") + .exchange() + .expectStatus() + .isOk(); + } + + @EnableAutoConfiguration + @SpringBootConfiguration + @Import(DefaultTestConfig.class) + public static class TestConfig { + + @Bean + public RouteLocator routeLocator(RouteLocatorBuilder builder) { + return builder.routes() + .route("map_subdomain_to_service_name", + r -> r.host("{serviceName}.setrequesturi.org") + .filters(f -> f.prefixPath("/httpbin").setRequestUri("lb://{serviceName}")) + .uri("no://op")) + .route("map_path_to_service_name", + r -> r.host("setrequesturi.org") + .and() + .path("/service/{serviceName}") + .filters(f -> f.rewritePath("/.*", "/").setRequestUri("lb://{serviceName}")) + .uri("no://op")) + .build(); + } + + } + +} diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/SetRequestUriGatewayFilterFactoryTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/SetRequestUriGatewayFilterFactoryTests.java new file mode 100644 index 00000000..2366b0c8 --- /dev/null +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/SetRequestUriGatewayFilterFactoryTests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2013-2020 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.net.URI; + +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import reactor.core.publisher.Mono; + +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.web.server.ServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR; + +/** + * @author Stepan Mikhailiuk + */ +public class SetRequestUriGatewayFilterFactoryTests { + + @Test + public void filterChangeRequestUri() { + SetRequestUriGatewayFilterFactory factory = new SetRequestUriGatewayFilterFactory(); + GatewayFilter filter = factory.apply(c -> c.setTemplate("https://example.com")); + MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost").build(); + ServerWebExchange exchange = MockServerWebExchange.from(request); + exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, URI.create("http://localhost")); + GatewayFilterChain filterChain = mock(GatewayFilterChain.class); + ArgumentCaptor captor = ArgumentCaptor.forClass(ServerWebExchange.class); + when(filterChain.filter(captor.capture())).thenReturn(Mono.empty()); + filter.filter(exchange, filterChain); + ServerWebExchange webExchange = captor.getValue(); + URI uri = (URI) webExchange.getAttributes().get(GATEWAY_REQUEST_URL_ATTR); + assertThat(uri).isNotNull(); + assertThat(uri.toString()).isEqualTo("https://example.com"); + } + + @Test + public void filterDoesNotChangeRequestUriIfUriIsInvalid() throws Exception { + SetRequestUriGatewayFilterFactory factory = new SetRequestUriGatewayFilterFactory(); + GatewayFilter filter = factory.apply(c -> c.setTemplate("invalid_uri")); + MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost").build(); + ServerWebExchange exchange = MockServerWebExchange.from(request); + exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, URI.create("http://localhost")); + GatewayFilterChain filterChain = mock(GatewayFilterChain.class); + ArgumentCaptor captor = ArgumentCaptor.forClass(ServerWebExchange.class); + when(filterChain.filter(captor.capture())).thenReturn(Mono.empty()); + filter.filter(exchange, filterChain); + ServerWebExchange webExchange = captor.getValue(); + URI uri = (URI) webExchange.getAttributes().get(GATEWAY_REQUEST_URL_ATTR); + assertThat(uri).isNotNull(); + assertThat(uri.toURL().toString()).isEqualTo("http://localhost"); + } + + @Test + public void toStringFormat() { + SetRequestUriGatewayFilterFactory.Config config = new SetRequestUriGatewayFilterFactory.Config(); + config.setTemplate("http://localhost:8080"); + GatewayFilter filter = new SetRequestUriGatewayFilterFactory().apply(config); + assertThat(filter.toString()).contains("http://localhost:8080"); + } + +} diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/test/AdhocTestSuite.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/test/AdhocTestSuite.java index 38041174..bba39646 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/test/AdhocTestSuite.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/test/AdhocTestSuite.java @@ -66,6 +66,8 @@ import static org.junit.Assume.assumeThat; RewriteLocationResponseHeaderGatewayFilterFactoryTests.class, org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactoryTests.class, org.springframework.cloud.gateway.filter.factory.RequestHeaderToRequestUriGatewayFilterFactoryIntegrationTests.class, + org.springframework.cloud.gateway.filter.factory.SetRequestUriGatewayFilterFactoryTests.class, + org.springframework.cloud.gateway.filter.factory.SetRequestUriGatewayFilterFactoryIntegrationTests.class, org.springframework.cloud.gateway.filter.factory.RemoveResponseHeaderGatewayFilterFactoryTests.class, org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactoryTests.class, org.springframework.cloud.gateway.filter.factory.StripPrefixGatewayFilterFactoryIntegrationTests.class,