diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/MvcUtils.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/MvcUtils.java index 9a45c79ef2..4d33fdc4b2 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/MvcUtils.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/MvcUtils.java @@ -21,6 +21,8 @@ import java.io.InputStream; import java.io.UncheckedIOException; import java.net.URI; +import java.net.URLDecoder; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collection; @@ -39,11 +41,13 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.StreamUtils; +import org.springframework.util.StringUtils; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.servlet.function.ServerRequest; import org.springframework.web.servlet.support.RequestContextUtils; @@ -283,6 +287,23 @@ public static MultiValueMap encodeQueryParams(MultiValueMap decodeQueryString(@Nullable String queryString, Charset charset) { + String[] pairs = StringUtils.tokenizeToStringArray(queryString, "&"); + MultiValueMap result = new LinkedMultiValueMap<>(pairs.length); + for (String pair : pairs) { + int idx = pair.indexOf('='); + if (idx == -1) { + result.add(URLDecoder.decode(pair, charset), null); + } + else { + String name = URLDecoder.decode(pair.substring(0, idx), charset); + String value = URLDecoder.decode(pair.substring(idx + 1), charset); + result.add(name, value); + } + } + return result; + } + private record ByteArrayInputMessage(ServerRequest request, ByteArrayInputStream body) implements HttpInputMessage { @Override diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/FormFilter.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/FormFilter.java index 9bccb89d8f..85218519fa 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/FormFilter.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/FormFilter.java @@ -42,13 +42,13 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequestWrapper; +import org.springframework.cloud.gateway.server.mvc.common.MvcUtils; import org.springframework.core.Ordered; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.lang.Nullable; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; -import org.springframework.web.util.UriComponentsBuilder; /** * Filter that rebuilds the body for form urlencoded posts. Serlvets treat query @@ -113,13 +113,7 @@ static HttpServletRequest getRequestWithBodyFromRequestParameters(HttpServletReq Writer writer = new OutputStreamWriter(bos, FORM_CHARSET); Map form = request.getParameterMap(); - String queryString = request.getQueryString(); - StringBuffer requestURL = request.getRequestURL(); - if (StringUtils.hasText(queryString)) { - requestURL.append('?').append(queryString); - } - UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(requestURL.toString()); - MultiValueMap queryParams = uriComponentsBuilder.build().getQueryParams(); + MultiValueMap queryParams = MvcUtils.decodeQueryString(request.getQueryString(), FORM_CHARSET); for (Iterator> entryIterator = form.entrySet().iterator(); entryIterator .hasNext();) { Map.Entry entry = entryIterator.next(); diff --git a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/FormFilterTests.java b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/FormFilterTests.java new file mode 100644 index 0000000000..d908783e63 --- /dev/null +++ b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/FormFilterTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2025-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.server.mvc.filter; + +import java.io.IOException; +import java.net.URI; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StreamUtils; +import org.springframework.util.StringUtils; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.verify; + +/** + * @author shawyeok + */ +class FormFilterTests { + + @Test + void hideFormParameterFromParameterMap() throws ServletException, IOException { + FormFilter filter = new FormFilter(); + MockHttpServletRequest request = MockMvcRequestBuilders + .post(URI.create("http://localhost/test?queryArg1=foo&queryArg2=%E4%BD%A0%E5%A5%BD")) + .contentType("application/x-www-form-urlencoded") + .content("formArg1=bar&formArg2=%7B%7D") + .buildRequest(null); + HttpServletResponse response = Mockito.mock(HttpServletResponse.class); + FilterChain chain = Mockito.mock(FilterChain.class); + filter.doFilter(request, response, chain); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ServletRequest.class); + verify(chain).doFilter(captor.capture(), Mockito.eq(response)); + HttpServletRequest servletRequest = (HttpServletRequest) captor.getValue(); + assertEquals("foo", servletRequest.getParameter("queryArg1")); + assertArrayEquals(new String[]{"foo"}, servletRequest.getParameterValues("queryArg1")); + // "你好" is hello in Chinese + assertEquals("你好", servletRequest.getParameter("queryArg2")); + assertArrayEquals(new String[]{"你好"}, servletRequest.getParameterValues("queryArg2")); + assertNull(servletRequest.getParameter("formArg1")); + assertNull(servletRequest.getParameter("formArg2")); + assertEquals(2, servletRequest.getParameterMap().size()); + assertEquals(List.of("queryArg1", "queryArg2"), toList(servletRequest.getParameterNames())); + assertEquals("application/x-www-form-urlencoded", servletRequest.getHeader("Content-Type")); + MultiValueMap form = readForm(servletRequest); + assertEquals(2, form.size()); + assertEquals(List.of("bar"), form.get("formArg1")); + assertEquals(List.of("{}"), form.get("formArg2")); + } + + static MultiValueMap readForm(HttpServletRequest request) { + try { + String body = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8); + String[] pairs = StringUtils.tokenizeToStringArray(body, "&"); + MultiValueMap result = new LinkedMultiValueMap<>(pairs.length); + for (String pair : pairs) { + int idx = pair.indexOf('='); + if (idx == -1) { + result.add(URLDecoder.decode(pair, StandardCharsets.UTF_8), null); + } + else { + String name = URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8); + String value = URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8); + result.add(name, value); + } + } + return result; + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + + static List toList(Enumeration values) { + List list = new ArrayList<>(); + while (values.hasMoreElements()) { + list.add(values.nextElement()); + } + return list; + } +} \ No newline at end of file