场景交代
在springboot中添加拦截器进行权限拦截时,需要获取请求参数进行验证。当参数在url后面时(queryString)获取参数进行验证之后程序正常运行。但是,当请求参数在请求体中的时候,通过流的方式将请求体取出参数进行验证之后,发现后续流程抛出错误:
Required request body is missing ... |
经过排查,发现ServletInputStream的流只能读取一次(参考:httpServletRequest中的流只能读取一次的原因)。
这就是为什么在拦截器中读取消息体之后,controller的@RequestBody注解无法获取参数的原因。
解决思路
既然知道了原因,那就可以想到一个大概思路了:可不可以把请求的body流换成可重复读的流?
答案是可以的。可以通过继承HttpServletRequestWrapper类进行。
解决方案
1. 继承HttpServletRequestWrapper
继承HttpServletRequestWrapper类,将请求体中的流copy一份出来,覆写getInputStream()和getReader()方法供外部使用。如下,每次调用覆写后的getInputStream()方法都是从复制出来的二进制数组中进行获取,这个二进制数组在对象存在期间一直存在,这样就实现了流的重复读取。
import java.io.BufferedReader; | |
import java.io.ByteArrayInputStream; | |
import java.io.IOException; | |
import java.io.InputStreamReader; | |
import javax.servlet.ReadListener; | |
import javax.servlet.ServletInputStream; | |
import javax.servlet.http.HttpServletRequest; | |
import javax.servlet.http.HttpServletRequestWrapper; | |
import org.springframework.util.StreamUtils; | |
/** | |
* | |
* 从请求体中获取参数请求包装类:<br> | |
* @author nick | |
* @version 5.0 since 2018年9月5日 | |
*/ | |
public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper{ | |
private byte[] requestBody = null;//用于将流保存下来 | |
public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException { | |
super(request); | |
requestBody = StreamUtils.copyToByteArray(request.getInputStream()); | |
} | |
public ServletInputStream getInputStream() throws IOException { | |
final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody); | |
return new ServletInputStream() { | |
public int read() throws IOException { | |
return bais.read(); | |
} | |
public boolean isFinished() { | |
return false; | |
} | |
public boolean isReady() { | |
return false; | |
} | |
public void setReadListener(ReadListener readListener) { | |
} | |
}; | |
} | |
public BufferedReader getReader() throws IOException{ | |
return new BufferedReader(new InputStreamReader(getInputStream())); | |
} | |
} | |
2. 替换原始request对象
现在可重复读取流的请求对象构造好了,但是需要在拦截器中获取,就需要将包装后的请求对象放在拦截器中。由于filter在interceptor之前执行,因此可以通过filter进行实现。
创建filer,在filter中对request对象用包装后的request替换。
import java.io.IOException; | |
import javax.servlet.Filter; | |
import javax.servlet.FilterChain; | |
import javax.servlet.FilterConfig; | |
import javax.servlet.ServletException; | |
import javax.servlet.ServletRequest; | |
import javax.servlet.ServletResponse; | |
import javax.servlet.annotation.WebFilter; | |
import javax.servlet.http.HttpServletRequest; | |
import com.znz.dns.controller.interceptor.auth.BodyReaderHttpServletRequestWrapper; | |
public class BodyReaderFilter implements Filter{ | |
public void init(FilterConfig filterConfig) throws ServletException { | |
// do nothing | |
} | |
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) | |
throws IOException, ServletException { | |
ServletRequest requestWrapper=null; | |
if(request instanceof HttpServletRequest) { | |
requestWrapper=new BodyReaderHttpServletRequestWrapper((HttpServletRequest)request); | |
} | |
if(requestWrapper==null) { | |
chain.doFilter(request, response); | |
}else { | |
chain.doFilter(requestWrapper, response); | |
} | |
} | |
public void destroy() { | |
// do nothing | |
} | |
} | |
配置filter
public FilterRegistrationBean<BodyReaderFilter> Filters() { | |
FilterRegistrationBean<BodyReaderFilter> registrationBean = new FilterRegistrationBean<BodyReaderFilter>(); | |
registrationBean.setFilter(new BodyReaderFilter()); | |
registrationBean.addUrlPatterns("/*"); | |
registrationBean.setName("koalaSignFilter"); | |
return registrationBean; | |
} |
3. 获取请求体
既然request对象流已经换成了wrapper reqest,那么流就可以重复读取了。接下来就是获取。
在拦截器中,直接从request中获取流,并进行读取:
/** | |
* 获取请求体内容 | |
* @return | |
* @throws IOException | |
*/ | |
private Map<String, Object> getParamsFromRequestBody(HttpServletRequest request) throws IOException { | |
BufferedReader reader = request.getReader(); | |
StringBuilder builder = new StringBuilder(); | |
try { | |
String line = null; | |
while((line = reader.readLine()) != null) { | |
builder.append(line); | |
} | |
String bodyString = builder.toString(); | |
return objectMapper.readValue(bodyString, Map.class); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} finally { | |
try { | |
reader.close(); | |
} catch (IOException e) { | |
e.printStackTrace(); | |
} | |
} | |
return new HashMap<>(); | |
} |
补充
在网上找了一些关于“springboot请求体中流不可重复读取问题”的相关文章,解决了自己的问题,但是觉得整个逻辑不怎么清晰(可能是自己没理解?) 因此,根据自己的思路重新整理了一遍。