一、背景
在项目开发中,有时候需要从请求体中获取参数,并对参数进行sql注入、xss攻击校验或者对参数做一些签名验证,这些验证逻辑一般都是统一放到过滤器或拦截器里,这样就不用每个接口都去重复编写验签的逻辑。
对于接口有可能接收不同类型的数据,对于表单数据来说,只要调用request的getParameterMap就能全部取出来。对于json数据来说,需要通过request的输入流去读取。但问题在于request的输入流只能读取一次不能重复读取,所以我们在过滤器或拦截器里读取了request的输入流之后,请求走到controller层时就会报错。而本文的目的就是介绍如何解决在这种场景下遇到HttpServletRequest的输入流只能读取一次的问题。
二、原因分析
HttpServletRequest 对象中调用getInputStream()方法获取输入流时得到的是一个InputStream对象,而实际类型是ServletInputStream,它继承于InputStream。InputStream的read()方法内部有一个postion,标志当前流被读取到的位置,每读取一次,该标志就会移动一次,如果读到最后,read()会返回-1,表示已经读取完了。如果想要重新读取则需要调用reset()方法,position就会移动到上次调用mark的位置,mark默认是0,所以就能从头再读了。调用reset()方法的前提是已经重写了reset()方法,当然能否reset也是有条件的,它取决于markSupported()方法是否返回true。但是InputStream默认不实现reset(),并且markSupported()默认也是返回false。
通过查看ServletInputStream源码得知,可以看到该类没有重写mark()
,reset()
以及markSupported()
方法,这样就无法重复读取流,这就是我们从request对象中获取的输入流就只能读取一次的原因。
三、解决方案
方案1、 自定义实现HttpServletRequestWrapper
HttpServletRequestWrapper是tomcat 提供的基于HTTP 的Servlet 请求包装类,继承自ServletRequestWrapper,并实现了HttpServletRequest,所以它本质上也是一个HttpServletRequest。可以看到该类的构造函数,调用了父类的构造,然后所有的执行方法,都会先调用_getHttpServletReques
获取到父类的HttpServletRequest
,再通过HttpServletRequest
获取请求中的信息。换一种方式来说,HttpServletRequestWrapper
就是一个请求包装类,我们可以通过该类对请求进行装饰,比如对参数、编码方式等等进行重新设置。
方案2、使用ContentCachingRequestWrapper
ContentCachingRequestWrapper是官方提供的HttpServletRequestWrapper的子类,通过名称可以看出,是对请求内容进行缓存,可以使用该类获取请求参数。
本篇文章主要是方案1的方式,有关ContentCachingRequestWrapper的内容可以自行查阅相关资料。
四、代码
新建 RequestWrapper.java 重写 HttpServletRequestWrapper,首先我们要定义一个容器,将输入流里面的数据存储到这个容器里,这个容器可以是数组或集合。然后我们重写getInputStream方法,每次都从这个容器里读数据,这样我们的输入流就可以读取任意次了。
package com.test.xxx.filter;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
public class RequestWrapper extends HttpServletRequestWrapper {
private final byte[] body;
public RequestWrapper(HttpServletRequest request) throws IOException {
super(request);
try (InputStream inputStream = request.getInputStream()) {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, length);
}
body = byteArrayOutputStream.toByteArray();
}
}
@Override
public ServletInputStream getInputStream() {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() throws IOException {
return byteArrayInputStream.read();
}
@Override
public boolean isFinished() {
return byteArrayInputStream.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener listener) {
}
};
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
public byte[] getBody() {
return this.body;
}
}
新建 CommonFilter.java,将自定义的HttpServletRequest实现类对象替换到请求链中(并不是所有的请求都需要替换,例如上传文件的请求,不需要缓存InputStream中的数据,这需要通过请求中的Content-type进行判断)在过滤器里将原生的HttpServletRequest对象替换成我们的RequestWrapper对象。
package com.test.xxx.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@Slf4j
@WebFilter
public class CommonFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
String contentType = servletRequest.getContentType();
if (servletRequest instanceof HttpServletRequest) {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String url = request.getRequestURI();
String method = request.getMethod();
if (contentType != null && contentType.contains(MediaType.MULTIPART_FORM_DATA_VALUE)) {
log.info("接口 {} 文件上传 不过滤", url);
filterChain.doFilter(servletRequest, servletResponse);
} else if (HttpMethod.POST.matches(method)) {
RequestWrapper requestWrapper = new RequestWrapper(request);
String body = new String(requestWrapper.getBody(), servletRequest.getCharacterEncoding());
log.info("接口 {} 请求方法 {} 过滤参数 {}", url, method, body);
filterChain.doFilter(requestWrapper, servletResponse);
} else if (HttpMethod.GET.matches(method)) {
String queryString = request.getQueryString();
log.info("接口 {} 请求方法 {} 过滤参数 {}", url, method, queryString);
filterChain.doFilter(servletRequest, servletResponse);
} else {
filterChain.doFilter(servletRequest, servletResponse);
}
} else {
filterChain.doFilter(servletRequest, servletResponse);
}
}
}