場景交代
在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());
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);
return new ServletInputStream() {
@Override
public int read() throws IOException {
return bais.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
}
@Override
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;
@WebFilter(filterName="bodyReaderFilter",urlPatterns="/*")
public class BodyReaderFilter implements Filter{
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// do nothing
}
@Override
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);
}
}
@Override
public void destroy() {
// do nothing
}
}
配置filter
@Bean
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請求體中流不可重復讀取問題”的相關文章,解決了自己的問題,但是覺得整個邏輯不怎么清晰(可能是自己沒理解?) 因此,根據自己的思路重新整理了一遍。