获取不到HttpServletRequest的请求流分析

jimmy 2019年02月19日 1,086次浏览

问题现象

目前在做的一个交易系统需要接收外部消息通知,通知url格式是 http://domain.com/xx.do 运行一直没问题,为了方便进行服务间路由处理在通知url后面增加了参数 clu=A ,称作带参数方式,类似http://domain.com/xx.do?clu=A 修改后发现通知报文验签失败,为了解决这个验签问题对http请求body的解析做了一些研究。

问题具体描述

目前获取通知参数使用的方式是 request.getParameter(p)

        Map<String, String> params = new HashMap<String, String>(request
                .getParameterMap().size());

        Enumeration<String> paramEnum = request.getParameterNames();
        while (paramEnum.hasMoreElements()) {
            String p = paramEnum.nextElement();
            params.put(p, request.getParameter(p));
        }

通过对params验签没有任何问题,但是通知地址修改为带参数方式后报文验签就不通过了。

初步判断

由于对方http通知报文头里面的 Content-Type是application/x-www-form-urlencoded 。那么对于request.getParameter()这种方式获取的value包含http请求的body里面的数据,也包含请求url后面的参数。但是验签是不包含url请求参数里面数据的,所以验签失败了。

详细原因

初步想法是只获取http请求的body并只对body里面的参数进行验签即可,本以为可以很快解决这个问题但是遇到了一些问题。

  1. 从request.getInputStream() 请求流中获取不到数据,特意查询了相关文档, 如果http请求的Content-Type是application/x-www-form-urlencoded 并且调用了request.getParameter() 方法后request.getInputStream()再也获取不到任何数据了。

  2. 将代码中获取流的request.getInputStream()代码移动到 request.getParameter()之前调用还是失败了,这和理论是不符合了,细究原因,由于项目使用的是spring boot方式,spring默认增加了filter OrderedHiddenHttpMethodFilter调用了request.getParameter(),导致流被消费掉。这个filter是通过WebMvcAutoConfiguration自动加载进来的,后面测试的时候把这个自动加载去掉就符合预期了。

一些梳理

测试基于tomcat-embed 9.0.13版本。 通过 org.apache.catalina.connector.Request 的getParameter方法可以看出,第一次调用的时候会尝试解析url和body的参数,但是也不是所有情况都解析。对于url后面的查询参数一定会解析的,对body有两种情况会解析。

/**
     * @return the value of the specified request parameter, if any; otherwise,
     * return <code>null</code>.  If there is more than one value defined,
     * return only the first one.
     *
     * @param name Name of the desired request parameter
     */
    @Override
    public String getParameter(String name) {

        if (!parametersParsed) {
            parseParameters();
        }

        return coyoteRequest.getParameters().getParameter(name);

    }

跳转到 parseParameters 方法。

 /**
     * Parse request parameters.
     */
    protected void parseParameters() {

        parametersParsed = true;

        Parameters parameters = coyoteRequest.getParameters();
        boolean success = false;
        try {
            // Set this every time in case limit has been changed via JMX
            parameters.setLimit(getConnector().getMaxParameterCount());

            // getCharacterEncoding() may have been overridden to search for
            // hidden form field containing request encoding
            Charset charset = getCharset();

            boolean useBodyEncodingForURI = connector.getUseBodyEncodingForURI();
            parameters.setCharset(charset);
            if (useBodyEncodingForURI) {
                parameters.setQueryStringCharset(charset);
            }
            // Note: If !useBodyEncodingForURI, the query string encoding is
            //       that set towards the start of CoyoyeAdapter.service()

            parameters.handleQueryParameters();

            if (usingInputStream || usingReader) {
                success = true;
                return;
            }

            String contentType = getContentType();
            if (contentType == null) {
                contentType = "";
            }
            int semicolon = contentType.indexOf(\';\');
            if (semicolon >= 0) {
                contentType = contentType.substring(0, semicolon).trim();
            } else {
                contentType = contentType.trim();
            }

            if ("multipart/form-data".equals(contentType)) {
                parseParts(false);
                success = true;
                return;
            }

            if( !getConnector().isParseBodyMethod(getMethod()) ) {
                success = true;
                return;
            }

            if (!("application/x-www-form-urlencoded".equals(contentType))) {
                success = true;
                return;
            }

            //=====解析body代码,省略了====
            success = true;
        } finally {
            if (!success) {
                parameters.setParseFailedReason(FailReason.UNKNOWN);
            }
        }

    }

可以看出只有 multipart/form-data 和application/x-www-form-urlencoded 两种Content-Type才会解析body。 其他的Content-Type不会解析body的,那么对其他Content-Type不管什么时候调用request.getInputStream()都能获取到期望的值,注意流只能消费一次。

另一个问题,对于出现多个重复key的情况可以使用request.getParameterValues获取一个string数组。 规则是url中的先解析,解析结果会在数组的前面,body里面的会在数组的后面,url和body中都可以出现重复的key。举个例子,,对于下面的请求,request.getParameterValues("mid")获取到的数据是 3 4 5 6

POST http://localhost:8080/test/servlet/parameter?mid=3&mid=4
Content-Type: application/x-www-form-urlencoded
accept-encoding: gzip, deflate
content-length: 11
mid=5&mid=6

解决

  1. 去掉Spring boot自动加载的filter,并且获取流的代码放在最前面,但是应用对filter机制依赖比较大,放弃了这种方案。
  2. 还是使用request.getParameter来获取数据,并去掉url里面的的参数,通过request.getQueryString获取,这里要考虑到body里面的key和url里面的key重复的情况,这个时候request.getParameterValues会返回多个,取最后一个就是body里面的值。

测试代码

@RequestMapping("test/parameter")
	@ResponseBody
	public String testHttpParameter(HttpServletRequest request, HttpServletResponse response){

		try {
			String body = IOUtils.toString(request.getReader());
			System.out.println("A: body is:" + body);
		} catch (IOException e) {
			e.printStackTrace();
		}
		String mid = request.getParameter("mid");
		System.out.println("mid is:" + mid);
		String[] mids = request.getParameterValues("mid");
		System.out.println("mids is:" + Arrays.asList(mids));

		try {
			String body = IOUtils.toString(request.getReader());
			System.out.println("B: body is:" + body);
		} catch (IOException e) {
			e.printStackTrace();
		}

		return "this is result";
	}

请输入图片描述