背景
通常對安全性有要求的接口都會對請求參數做一些簽名驗證,而我們一般會把驗簽的邏輯統一放到過濾器或攔截器里,這樣就不用每個接口都去重復編寫驗簽的邏輯。
在一個項目中會有很多的接口,而不同的接口可能接收不同類型的數據,例如表單數據和json數據,表單數據還好說,調用request的getParameterMap就能全部取出來。而json數據就有些麻煩了,因為json數據放在body中,我們需要通過request的輸入流去讀取。
但問題在於request的輸入流只能讀取一次不能重復讀取,所以我們在過濾器或攔截器里讀取了request的輸入流之后,請求走到controller層時就會報錯。而本文的目的就是介紹如何解決在這種場景下遇到HttpServletRequest的輸入流只能讀取一次的問題。
注:本文代碼基於SpringBoot框架
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()方法:
綜上,InputStream默認不實現reset的相關方法,而ServletInputStream也沒有重寫reset的相關方法,這樣就無法重復讀取流,這就是我們從request對象中獲取的輸入流就只能讀取一次的原因。
使用HttpServletRequestWrapper + Filter解決輸入流不能重復讀取問題
既然ServletInputStream不支持重新讀寫,那么為什么不把流讀出來后用容器存儲起來,后面就可以多次利用了。那么問題就來了,要如何存儲這個流呢?
所幸JavaEE提供了一個 HttpServletRequestWrapper類,從類名也可以知道它是一個http請求包裝器,其基於裝飾者模式實現了HttpServletRequest界面,部分源碼如下:
從上圖中的部分源碼可以看到,該類並沒有真正去實現HttpServletRequest的方法,而只是在方法內又去調用HttpServletRequest的方法,所以我們可以通過繼承該類並實現想要重新定義的方法以達到包裝原生HttpServletRequest對象的目的。
首先我們要定義一個容器,將輸入流里面的數據存儲到這個容器里,這個容器可以是數組或集合。然后我們重寫getInputStream方法,每次都從這個容器里讀數據,這樣我們的輸入流就可以讀取任意次了。
實現代碼:
package com.eshore.ismp.filter;
import org.springframework.util.StreamUtils;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
/**
* Created by lihaodi on 2016/4/7.
*/
public class RequestWrapper extends HttpServletRequestWrapper {
private byte[] requestBody;
public RequestWrapper(HttpServletRequest request) throws IOException {
super(request);
requestBody = StreamUtils.copyToByteArray(request.getInputStream());
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8));
}
@Override
public ServletInputStream getInputStream() {
if (requestBody == null) {
requestBody = new byte[0];
}
final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() throws IOException {
return bais.read();
}
};
}
}
