diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/BeforeFilterFunctions.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/BeforeFilterFunctions.java index 3c86e394..98f0bbc8 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/BeforeFilterFunctions.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/BeforeFilterFunctions.java @@ -45,7 +45,6 @@ import org.springframework.web.server.ResponseStatusException; import org.springframework.web.servlet.function.ServerRequest; import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriTemplate; -import org.springframework.web.util.UriUtils; import static org.springframework.cloud.gateway.server.mvc.common.MvcUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR; import static org.springframework.util.CollectionUtils.unmodifiableMultiValueMap; @@ -216,7 +215,7 @@ public abstract class BeforeFilterFunctions { MultiValueMap queryParams = new LinkedMultiValueMap<>(request.params()); queryParams.remove(name); - MultiValueMap encodedQueryParams = UriUtils.encodeQueryParams(queryParams); + MultiValueMap encodedQueryParams = MvcUtils.encodeQueryParams(queryParams); // remove from uri URI newUri = UriComponentsBuilder.fromUri(request.uri()) diff --git a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/BeforeFilterFunctionsTests.java b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/BeforeFilterFunctionsTests.java index a8777c56..c90b256c 100644 --- a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/BeforeFilterFunctionsTests.java +++ b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/BeforeFilterFunctionsTests.java @@ -119,6 +119,7 @@ class BeforeFilterFunctionsTests { MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/path") .queryParam("foo", "bar") .queryParam("baz[]", "qux[]") + .queryParam("quux", "corge+") .buildRequest(null); ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList()); @@ -127,7 +128,8 @@ class BeforeFilterFunctionsTests { assertThat(result.param("foo")).isEmpty(); assertThat(result.param("baz[]")).isPresent().hasValue("qux[]"); - assertThat(result.uri().toString()).hasToString("http://localhost/path?baz%5B%5D=qux%5B%5D"); + assertThat(result.param("quux")).isPresent().hasValue("corge+"); + assertThat(result.uri().toString()).hasToString("http://localhost/path?baz%5B%5D=qux%5B%5D&quux=corge%2B"); } @Test diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/RemoveRequestParameterGatewayFilterFactory.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/RemoveRequestParameterGatewayFilterFactory.java index 9d17f6ef..b8469337 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/RemoveRequestParameterGatewayFilterFactory.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/RemoveRequestParameterGatewayFilterFactory.java @@ -24,12 +24,12 @@ import reactor.core.publisher.Mono; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.support.ServerWebExchangeUtils; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.util.UriComponentsBuilder; -import org.springframework.web.util.UriUtils; import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator; import static org.springframework.util.CollectionUtils.unmodifiableMultiValueMap; @@ -59,7 +59,8 @@ public class RemoveRequestParameterGatewayFilterFactory queryParams.remove(config.getName()); try { - MultiValueMap encodedQueryParams = UriUtils.encodeQueryParams(queryParams); + MultiValueMap encodedQueryParams = ServerWebExchangeUtils + .encodeQueryParams(queryParams); URI newUri = UriComponentsBuilder.fromUri(request.getURI()) .replaceQueryParams(unmodifiableMultiValueMap(encodedQueryParams)) .build(true) diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/RewriteRequestParameterGatewayFilterFactory.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/RewriteRequestParameterGatewayFilterFactory.java index 02bccdaf..18f5e588 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/RewriteRequestParameterGatewayFilterFactory.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/RewriteRequestParameterGatewayFilterFactory.java @@ -24,13 +24,13 @@ import reactor.core.publisher.Mono; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.support.ServerWebExchangeUtils; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.util.UriComponentsBuilder; -import org.springframework.web.util.UriUtils; import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator; import static org.springframework.util.CollectionUtils.unmodifiableMultiValueMap; @@ -71,7 +71,8 @@ public class RewriteRequestParameterGatewayFilterFactory } try { - MultiValueMap encodedQueryParams = UriUtils.encodeQueryParams(queryParams); + MultiValueMap encodedQueryParams = ServerWebExchangeUtils + .encodeQueryParams(queryParams); URI uri = uriComponentsBuilder.replaceQueryParams(unmodifiableMultiValueMap(encodedQueryParams)) .build(true) .toUri(); diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/support/ServerWebExchangeUtils.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/support/ServerWebExchangeUtils.java index 7585a182..fd185504 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/support/ServerWebExchangeUtils.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/support/ServerWebExchangeUtils.java @@ -17,9 +17,11 @@ package org.springframework.cloud.gateway.support; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; @@ -48,9 +50,13 @@ import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequestDecorator; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.util.UriUtils; /** * @author Spencer Gibb @@ -260,6 +266,17 @@ public final class ServerWebExchangeUtils { return encoded; } + public static MultiValueMap encodeQueryParams(MultiValueMap params) { + MultiValueMap encodedQueryParams = new LinkedMultiValueMap<>(params.size()); + for (Map.Entry> entry : params.entrySet()) { + for (String value : entry.getValue()) { + encodedQueryParams.add(UriUtils.encode(entry.getKey(), StandardCharsets.UTF_8), + UriUtils.encode(value, StandardCharsets.UTF_8)); + } + } + return CollectionUtils.unmodifiableMultiValueMap(encodedQueryParams); + } + public static HttpStatus parse(String statusString) { HttpStatus httpStatus; diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpointRedisRefreshTest.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpointRedisRefreshTest.java index 1d630bac..b2b331bf 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpointRedisRefreshTest.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpointRedisRefreshTest.java @@ -49,8 +49,8 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen /** * @author Peter Müller */ -@SpringBootTest(properties = {"management.endpoint.gateway.enabled=true", - "management.endpoints.web.exposure.include=*", "spring.cloud.gateway.actuator.verbose.enabled=true"}, +@SpringBootTest(properties = { "management.endpoint.gateway.enabled=true", + "management.endpoints.web.exposure.include=*", "spring.cloud.gateway.actuator.verbose.enabled=true" }, webEnvironment = RANDOM_PORT) @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) @ActiveProfiles("redis-route-repository") @@ -94,8 +94,9 @@ public class GatewayControllerEndpointRedisRefreshTest { createOrUpdateRouteWithCors(cors); Awaitility.await().atMost(Duration.ofSeconds(3)).untilAsserted(() -> assertRouteHasCorsConfig(cors)); - Awaitility.await().atMost(Duration.ofSeconds(3)) - .untilAsserted(() -> assertPreflightAllowOrigin("http://example.org")); + Awaitility.await() + .atMost(Duration.ofSeconds(3)) + .untilAsserted(() -> assertPreflightAllowOrigin("http://example.org")); } void createOrUpdateRouteWithCors(Map cors) { @@ -108,41 +109,41 @@ public class GatewayControllerEndpointRedisRefreshTest { testRouteDefinition.setMetadata(Map.of("cors", cors)); testClient.post() - .uri("http://localhost:" + port + "/actuator/gateway/routes/cors-test-route") - .accept(MediaType.APPLICATION_JSON) - .body(BodyInserters.fromValue(testRouteDefinition)) - .exchange() - .expectStatus() - .isCreated(); + .uri("http://localhost:" + port + "/actuator/gateway/routes/cors-test-route") + .accept(MediaType.APPLICATION_JSON) + .body(BodyInserters.fromValue(testRouteDefinition)) + .exchange() + .expectStatus() + .isCreated(); testClient.post() - .uri("http://localhost:" + port + "/actuator/gateway/refresh") - .exchange() - .expectStatus() - .isOk(); + .uri("http://localhost:" + port + "/actuator/gateway/refresh") + .exchange() + .expectStatus() + .isOk(); } void assertRouteHasCorsConfig(Map cors) { testClient.get() - .uri("http://localhost:" + port + "/actuator/gateway/routes/cors-test-route") - .exchange() - .expectStatus() - .isOk() - .expectBody() - .jsonPath("$.metadata") - .value(map -> assertThat((Map) map).hasSize(1) - .containsEntry("cors", cors)); + .uri("http://localhost:" + port + "/actuator/gateway/routes/cors-test-route") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .jsonPath("$.metadata") + .value(map -> assertThat((Map) map).hasSize(1).containsEntry("cors", cors)); } void assertPreflightAllowOrigin(String origin) { testClient.options() - .uri("http://localhost:" + port + "/") - .header("Origin", "http://example.org") - .header("Access-Control-Request-Method", "GET") - .exchange() - .expectStatus() - .isOk() - .expectHeader() - .valueEquals("Access-Control-Allow-Origin", origin); + .uri("http://localhost:" + port + "/") + .header("Origin", "http://example.org") + .header("Access-Control-Request-Method", "GET") + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .valueEquals("Access-Control-Allow-Origin", origin); } + } diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/RemoveRequestParameterGatewayFilterFactoryTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/RemoveRequestParameterGatewayFilterFactoryTests.java index a96aef38..2eb96bb6 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/RemoveRequestParameterGatewayFilterFactoryTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/RemoveRequestParameterGatewayFilterFactoryTests.java @@ -16,6 +16,8 @@ package org.springframework.cloud.gateway.filter.factory; +import java.net.URI; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -24,6 +26,7 @@ import reactor.core.publisher.Mono; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory.NameConfig; +import org.springframework.http.HttpMethod; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.web.server.MockServerWebExchange; @@ -123,6 +126,23 @@ class RemoveRequestParameterGatewayFilterFactoryTests { assertThat(actualRequest.getQueryParams()).containsEntry("ccc", singletonList(",xyz")); } + @Test + void removeRequestParameterFilterShouldHandleRemainingPlusSignParams() { + MockServerHttpRequest request = MockServerHttpRequest + .method(HttpMethod.GET, URI.create("http://localhost?foo=bar&aaa=%2Bxyz")) + .build(); + exchange = MockServerWebExchange.from(request); + NameConfig config = new NameConfig(); + config.setName("foo"); + GatewayFilter filter = new RemoveRequestParameterGatewayFilterFactory().apply(config); + + filter.filter(exchange, filterChain); + + ServerHttpRequest actualRequest = captor.getValue().getRequest(); + assertThat(actualRequest.getQueryParams()).doesNotContainKey("foo"); + assertThat(actualRequest.getQueryParams()).containsEntry("aaa", singletonList("+xyz")); + } + @Test void removeRequestParameterFilterShouldHandleEncodedParameterName() { MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost") diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/RewriteRequestParameterGatewayFilterFactoryTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/RewriteRequestParameterGatewayFilterFactoryTests.java index f7803861..24aa900b 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/RewriteRequestParameterGatewayFilterFactoryTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/RewriteRequestParameterGatewayFilterFactoryTests.java @@ -88,6 +88,12 @@ class RewriteRequestParameterGatewayFilterFactoryTests { Map.of("campaign[]", List.of("blue"), "color", List.of("white"))); } + @Test + void rewriteRequestParameterFilterWithPlusSign() { + testRewriteRequestParameterFilter("color", "white+", "campaign=blue%2B&color=green", + Map.of("campaign", List.of("blue+"), "color", List.of("white+"))); + } + private void testRewriteRequestParameterFilter(String name, String replacement, String query, Map> expectedQueryParams) { GatewayFilter filter = new RewriteRequestParameterGatewayFilterFactory()