場景交代
在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請求體中流不可重復讀取問題”的相關文章,解決了自己的問題,但是覺得整個邏輯不怎么清晰(可能是自己沒理解?) 因此,根據自己的思路重新整理了一遍。