Spring boot 2.0.0 升级到 2.4.1 , POST 请求提交表单无法获取表单参数的问题

描述

在通过POST请求提交Form Data的表单参数,并且Content-Type: application/x-www-form-urlencoded; charset=UTF-8时,在请求处理器中,在Request 中无法获取对应的参数对象。

请求样例:

Alt text

请求处理器获取的代码:

if ("/submitLogin".equals(path)) {
     String usernameParam = request.getParameter(PARAM_NAME_USERNAME);
     String passwordParam = request.getParameter(PARAM_NAME_PASSWORD);
     if (username.equals(usernameParam) && password.equals(passwordParam)) {
         request.getSession().setAttribute(SESSION_USER_KEY, username);
         response.getWriter().print("success");
     } else {
         response.getWriter().print("error");
     }
     return;
 }

分析

1、 调研request.getParameter(PARAM_NAME_USERNAME) 的参数从哪里来

通过跟踪调用栈,发现是从org.apache.coyote.Request#getParameters中获取参数集合,调用栈如下:

getParameter:1130, Request (org.apache.catalina.connector)
getParameter:381, RequestFacade (org.apache.catalina.connector)
getParameter:158, ServletRequestWrapper (javax.servlet)
service:305, ResourceServlet$ResourceHandler (com.alibaba.druid.support.http)
service:130, ResourceServlet (com.alibaba.druid.support.http)
service:223, StatViewServlet (com.alibaba.druid.support.http)
service:733, HttpServlet (javax.servlet.http)

跟踪发现org.apache.tomcat.util.http.Parameters#paramHashValues的值为空。

2、 调研org.apache.tomcat.util.http.Parameters#paramHashValues 的值在什么地方设置

调研后发现,值的设置只有org.apache.tomcat.util.http.Parameters#addParameter 这个一个方法。继续跟踪该方法的调用链

发现了两个地方调用了该方法,分别是:org.apache.tomcat.util.http.Parameters#processParameters(byte[], int, int, java.nio.charset.Charset)org.apache.catalina.connector.Request#parseParts

org.apache.tomcat.util.http.Parameters#processParameters(byte[], int, int, java.nio.charset.Charset)的调用链: org.apache.catalina.connector.Request#getParameter ->org.apache.catalina.connector.Request#parseParameters ->org.apache.tomcat.util.http.Parameters#processParameters(byte[], int, int)

org.apache.catalina.connector.Request#parseParameters ->org.apache.tomcat.util.http.Parameters#handleQueryParameters ->org.apache.tomcat.util.http.Parameters#processParameters(org.apache.tomcat.util.buf.MessageBytes, java.nio.charset.Charset)

从调用链反推的调研效率较差,因此该方法先搁置。

3、 通过比对Spring Boot 的不同版本对POST提交Form表单的请求的处理

经过调研发现,Request中的Parameter是在Filter中被加工好了参数值,通过对吧2.0.0版本和2.4.1版本的Filter Chain,发现了问题。

Spring Boot 2.0.0 版本的Filter Chain:

0 = {ApplicationFilterConfig@11758} "ApplicationFilterConfig[name=characterEncodingFilter, filterClass=org.springframework.boot.web.servlet.filter.OrderedCharacterEncodingFilter]"
1 = {ApplicationFilterConfig@11747} "ApplicationFilterConfig[name=hiddenHttpMethodFilter, filterClass=org.springframework.boot.web.servlet.filter.OrderedHiddenHttpMethodFilter]"
2 = {ApplicationFilterConfig@11790} "ApplicationFilterConfig[name=httpPutFormContentFilter, filterClass=org.springframework.boot.web.servlet.filter.OrderedHttpPutFormContentFilter]"
3 = {ApplicationFilterConfig@11791} "ApplicationFilterConfig[name=requestContextFilter, filterClass=org.springframework.boot.web.servlet.filter.OrderedRequestContextFilter]"
4 = {ApplicationFilterConfig@11792} "ApplicationFilterConfig[name=sessionFilter, filterClass=com.abc.springboot.frame.filter.HttpServletRequestReplacedFilter]"
5 = {ApplicationFilterConfig@11793} "ApplicationFilterConfig[name=webStatFilter, filterClass=com.alibaba.druid.support.http.WebStatFilter]"
6 = {ApplicationFilterConfig@11794} "ApplicationFilterConfig[name=corsFilter, filterClass=org.springframework.web.filter.CorsFilter]"
7 = {ApplicationFilterConfig@11795} "ApplicationFilterConfig[name=Tomcat WebSocket (JSR356) Filter, filterClass=org.apache.tomcat.websocket.server.WsFilter]"

Spring Boot 2.4.1 版本的Filter Chain:

0 = {ApplicationFilterConfig@14146} "ApplicationFilterConfig[name=characterEncodingFilter, filterClass=org.springframework.boot.web.servlet.filter.OrderedCharacterEncodingFilter]"
1 = {ApplicationFilterConfig@14199} "ApplicationFilterConfig[name=webMvcMetricsFilter, filterClass=org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter]"
2 = {ApplicationFilterConfig@14221} "ApplicationFilterConfig[name=formContentFilter, filterClass=org.springframework.boot.web.servlet.filter.OrderedFormContentFilter]"
3 = {ApplicationFilterConfig@14222} "ApplicationFilterConfig[name=requestContextFilter, filterClass=org.springframework.boot.web.servlet.filter.OrderedRequestContextFilter]"
4 = {ApplicationFilterConfig@14223} "ApplicationFilterConfig[name=sessionFilter, filterClass=com.abc.springboot.frame.filter.HttpServletRequestReplacedFilter]"
5 = {ApplicationFilterConfig@14224} "ApplicationFilterConfig[name=webStatFilter, filterClass=com.alibaba.druid.support.http.WebStatFilter]"
6 = {ApplicationFilterConfig@14225} "ApplicationFilterConfig[name=corsFilter, filterClass=org.springframework.web.filter.CorsFilter]"
7 = {ApplicationFilterConfig@14226} "ApplicationFilterConfig[name=Tomcat WebSocket (JSR356) Filter, filterClass=org.apache.tomcat.websocket.server.WsFilter]"

通过跟踪Filter Chain的执行,最终发现Form 参数的解析处理是在org.springframework.boot.web.servlet.filter.OrderedHiddenHttpMethodFilter 完成的。

4、调研org.springframework.boot.web.servlet.filter.OrderedHiddenHttpMethodFilter实现

org.springframework.boot.web.servlet.filter.OrderedHiddenHttpMethodFilter 继承了org.springframework.web.filter.HiddenHttpMethodFilter

注意和org.springframework.boot.web.reactive.filter下的OrderedHiddenHttpMethodFilter类区分。

HiddenHttpMethodFilter#doFilterInternal的方法中,调用了实际的方法处理代码

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {

		HttpServletRequest requestToUse = request;

		if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
			//调用了解析Form data参数的处理
			String paramValue = request.getParameter(this.methodParam);
			if (StringUtils.hasLength(paramValue)) {
				String method = paramValue.toUpperCase(Locale.ENGLISH);
				if (ALLOWED_METHODS.contains(method)) {
					requestToUse = new HttpMethodRequestWrapper(request, method);
				}
			}
		}

		filterChain.doFilter(requestToUse, response);
	}

5、调研org.springframework.boot.web.servlet.filter.OrderedHiddenHttpMethodFilter 为什么没有加入Filter Chain

调研后发现,是由org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration配置类完成自动引入org.springframework.boot.web.servlet.filter.OrderedHiddenHttpMethodFilter的。 通过对比Spring Boot 2.0.02.4.1版本的该类的代码,发现了问题所在。

2.0.0的代码:

	@Bean
	@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
	public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
		return new OrderedHiddenHttpMethodFilter();
	}

2.4.1的代码:

	@Bean
	@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
	@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
	public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
		return new OrderedHiddenHttpMethodFilter();
	}

2.0.0版本中默认是启用了OrderedHiddenHttpMethodFilter,但在2.4.1版本中默认是不启用,需要通过配置spring.mvc.hiddenmethod.filter.enabled=true 来启用。

解决方案

增加配置:spring.mvc.hiddenmethod.filter.enabled=true

总结

该问题影响到所有通过POST 请求发送Content-Type: application/x-www-form-urlencoded的请求,比如Druid 数据源的监控登录。