diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 599786b797..ae62b7c765 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -41,6 +41,7 @@ *** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/setrequestheader-factory.adoc[] *** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/setresponseheader-factory.adoc[] *** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/setstatus-factory.adoc[] +*** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/stripcontextpath-factory.adoc[] *** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/stripprefix-factory.adoc[] *** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/retry-factory.adoc[] *** xref:spring-cloud-gateway-server-webflux/gatewayfilter-factories/requestsize-factory.adoc[] diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/stripcontextpath-factory.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/stripcontextpath-factory.adoc new file mode 100644 index 0000000000..13f28877a5 --- /dev/null +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-webflux/gatewayfilter-factories/stripcontextpath-factory.adoc @@ -0,0 +1,28 @@ +[[stripcontextpath-gatewayfilter-factory]] += `StripContextPath` `GatewayFilter` Factory + +The `StripContextPath` `GatewayFilter` factory removes the request context path before downstream path filters run. +This is useful when a context path is present through `spring.webflux.base-path` or forwarded prefixes and you want filters such as `StripPrefix`, `RewritePath`, `SetPath`, or `PrefixPath` to operate on the application path instead. + +The `StripContextPath` filter must be listed before any path-manipulating filters. +The following example configures a `StripContextPath` `GatewayFilter`: + +.application.yml +[source,yaml] +---- +spring: + cloud: + gateway: + server: + webflux: + routes: + - id: strip_context_path_route + uri: https://example.org + predicates: + - Path=/name/** + filters: + - StripContextPath + - StripPrefix=1 +---- + +With a context path of `/context`, a request to `/context/name/blue` first becomes `/name/blue`, and then `StripPrefix=1` sends `https://example.org/blue` downstream. diff --git a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index 934f190224..344fd582a9 100644 --- a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java +++ b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java @@ -119,6 +119,7 @@ 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.StripContextPathGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.StripPrefixGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.TokenRelayGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.rewrite.GzipMessageBodyResolver; @@ -724,6 +725,12 @@ public SetStatusGatewayFilterFactory setStatusGatewayFilterFactory() { return new SetStatusGatewayFilterFactory(); } + @Bean + @ConditionalOnEnabledFilter + public StripContextPathGatewayFilterFactory stripContextPathGatewayFilterFactory() { + return new StripContextPathGatewayFilterFactory(); + } + @Bean @ConditionalOnEnabledFilter public SaveSessionGatewayFilterFactory saveSessionGatewayFilterFactory() { diff --git a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/StripContextPathGatewayFilterFactory.java b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/StripContextPathGatewayFilterFactory.java new file mode 100644 index 0000000000..8507e964d5 --- /dev/null +++ b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/StripContextPathGatewayFilterFactory.java @@ -0,0 +1,73 @@ +/* + * Copyright 2013-present 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 reactor.core.publisher.Mono; + +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.util.StringUtils; +import org.springframework.web.server.ServerWebExchange; + +import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.addOriginalRequestUrl; + +/** + * Strips the request context path before later path-manipulating filters run. + * + * @author Garvit Joshi + */ +public class StripContextPathGatewayFilterFactory extends AbstractGatewayFilterFactory { + + public GatewayFilter apply() { + return apply(new Object()); + } + + @Override + public GatewayFilter apply(Object config) { + return new GatewayFilter() { + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + ServerHttpRequest request = exchange.getRequest(); + String contextPath = request.getPath().contextPath().value(); + if (!StringUtils.hasText(contextPath)) { + return chain.filter(exchange); + } + + addOriginalRequestUrl(exchange, request.getURI()); + + String newPath = request.getPath().pathWithinApplication().value(); + if (!StringUtils.hasText(newPath)) { + newPath = "/"; + } + + ServerHttpRequest newRequest = request.mutate().contextPath("").path(newPath).build(); + exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, newRequest.getURI()); + + return chain.filter(exchange.mutate().request(newRequest).build()); + } + + @Override + public String toString() { + return filterToStringCreator(StripContextPathGatewayFilterFactory.this).toString(); + } + }; + } + +} diff --git a/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/config/conditional/DisableBuiltInFiltersTests.java b/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/config/conditional/DisableBuiltInFiltersTests.java index 169eded273..c462eb968b 100644 --- a/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/config/conditional/DisableBuiltInFiltersTests.java +++ b/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/config/conditional/DisableBuiltInFiltersTests.java @@ -108,6 +108,7 @@ public void shouldInjectOnlyEnabledBuiltInFilters() { "spring.cloud.gateway.server.webflux.filter.rewrite-request-parameter.enabled=false", "spring.cloud.gateway.server.webflux.filter.set-status.enabled=false", "spring.cloud.gateway.server.webflux.filter.save-session.enabled=false", + "spring.cloud.gateway.server.webflux.filter.strip-context-path.enabled=false", "spring.cloud.gateway.server.webflux.filter.strip-prefix.enabled=false", "spring.cloud.gateway.server.webflux.filter.request-header-to-request-uri.enabled=false", "spring.cloud.gateway.server.webflux.filter.request-size.enabled=false", diff --git a/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/factory/StripContextPathGatewayFilterFactoryTests.java b/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/factory/StripContextPathGatewayFilterFactoryTests.java new file mode 100644 index 0000000000..90bcd8c64a --- /dev/null +++ b/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/factory/StripContextPathGatewayFilterFactoryTests.java @@ -0,0 +1,145 @@ +/* + * Copyright 2013-present 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.LinkedHashSet; + +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_ORIGINAL_REQUEST_URL_ATTR; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR; + +/** + * @author Garvit Joshi + */ +public class StripContextPathGatewayFilterFactoryTests { + + @Test + public void stripContextPathFilterWorks() { + MockServerHttpRequest request = request("/context/service/path", "/context"); + + ServerWebExchange exchange = filterAndGetExchange(new StripContextPathGatewayFilterFactory().apply(), + MockServerWebExchange.from(request)); + + assertThat(exchange.getRequest().getURI()).hasPath("/service/path"); + assertThat(exchange.getRequest().getPath().contextPath().value()).isEmpty(); + + URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR); + assertThat(requestUrl).hasScheme("http").hasHost("localhost").hasNoPort().hasPath("/service/path"); + LinkedHashSet uris = exchange.getRequiredAttribute(GATEWAY_ORIGINAL_REQUEST_URL_ATTR); + assertThat(uris).contains(request.getURI()); + } + + @Test + public void stripContextPathFilterToRootWorks() { + ServerWebExchange exchange = filterAndGetExchange(new StripContextPathGatewayFilterFactory().apply(), + MockServerWebExchange.from(request("/context", "/context"))); + + assertThat(exchange.getRequest().getURI()).hasPath("/"); + assertThat(exchange.getRequest().getPath().contextPath().value()).isEmpty(); + } + + @Test + public void stripContextPathFilterWithoutContextPathIsNoOp() { + ServerWebExchange exchange = filterAndGetExchange(new StripContextPathGatewayFilterFactory().apply(), + MockServerWebExchange.from(MockServerHttpRequest.get("http://localhost/service/path").build())); + + assertThat(exchange.getRequest().getURI()).hasPath("/service/path"); + assertThat(exchange.getRequest().getPath().contextPath().value()).isEmpty(); + assertThat(exchange.getAttributes()).doesNotContainKey(GATEWAY_ORIGINAL_REQUEST_URL_ATTR); + } + + @Test + public void stripContextPathThenStripPrefixWorks() { + ServerWebExchange exchange = stripContextPathExchange("/context/service/blue"); + + exchange = filterAndGetExchange(new StripPrefixGatewayFilterFactory().apply(c -> c.setParts(1)), exchange); + + assertThat(exchange.getRequest().getURI()).hasPath("/blue"); + assertThat(exchange.getRequest().getPath().contextPath().value()).isEmpty(); + } + + @Test + public void stripContextPathThenRewritePathWorks() { + ServerWebExchange exchange = stripContextPathExchange("/context/service/blue"); + + exchange = filterAndGetExchange(new RewritePathGatewayFilterFactory() + .apply(c -> c.setRegexp("/service/(?.*)").setReplacement("/${remaining}")), exchange); + + assertThat(exchange.getRequest().getURI()).hasPath("/blue"); + assertThat(exchange.getRequest().getPath().contextPath().value()).isEmpty(); + } + + @Test + public void stripContextPathThenPrefixPathWorks() { + ServerWebExchange exchange = stripContextPathExchange("/context/get"); + + exchange = filterAndGetExchange(new PrefixPathGatewayFilterFactory().apply(c -> c.setPrefix("/prefix")), + exchange); + + assertThat(exchange.getRequest().getURI()).hasPath("/prefix/get"); + assertThat(exchange.getRequest().getPath().contextPath().value()).isEmpty(); + } + + @Test + public void stripContextPathThenSetPathWorks() { + ServerWebExchange exchange = stripContextPathExchange("/context/service/blue"); + + exchange = filterAndGetExchange(new SetPathGatewayFilterFactory().apply(c -> c.setTemplate("/blue")), exchange); + + assertThat(exchange.getRequest().getURI()).hasPath("/blue"); + assertThat(exchange.getRequest().getPath().contextPath().value()).isEmpty(); + } + + @Test + public void toStringFormat() { + GatewayFilter filter = new StripContextPathGatewayFilterFactory().apply(); + assertThat(filter.toString()).contains("StripContextPath"); + } + + private ServerWebExchange stripContextPathExchange(String path) { + return filterAndGetExchange(new StripContextPathGatewayFilterFactory().apply(), + MockServerWebExchange.from(request(path, "/context"))); + } + + private MockServerHttpRequest request(String path, String contextPath) { + return MockServerHttpRequest.get("http://localhost" + path).contextPath(contextPath).build(); + } + + private ServerWebExchange filterAndGetExchange(GatewayFilter filter, ServerWebExchange exchange) { + GatewayFilterChain filterChain = mock(GatewayFilterChain.class); + ArgumentCaptor captor = ArgumentCaptor.forClass(ServerWebExchange.class); + when(filterChain.filter(captor.capture())).thenReturn(Mono.empty()); + + filter.filter(exchange, filterChain); + + return captor.getValue(); + } + +}