解決HttpServletRequest的輸入流只能讀取一次的問題
背景
通常對安全性有要求的介面都會對請求引數做一些簽名驗證,而我們一般會把驗籤的邏輯統一放到過濾器或攔截器裡,這樣就不用每個介面都去重複編寫驗籤的邏輯。
在一個專案中會有很多的介面,而不同的介面可能接收不同型別的資料,例如表單資料和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.example.wrapperdemo.controller.wrapper; import lombok.extern.slf4j.Slf4j; import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.*; import java.nio.charset.Charset; /** * @author 01 * @program wrapper-demo * @description 包裝HttpServletRequest,目的是讓其輸入流可重複讀 * @create 2018-12-24 20:48 * @since 1.0 **/ @Slf4j public class RequestWrapper extends HttpServletRequestWrapper { /** * 儲存body資料的容器 */ private final byte[] body; public RequestWrapper(HttpServletRequest request) throws IOException { super(request); // 將body資料儲存起來 String bodyStr = getBodyString(request); body = bodyStr.getBytes(Charset.defaultCharset()); } /** * 獲取請求Body * * @param request request * @return String */ public String getBodyString(final ServletRequest request) { try { return inputStream2String(request.getInputStream()); } catch (IOException e) { log.error("", e); throw new RuntimeException(e); } } /** * 獲取請求Body * * @return String */ public String getBodyString() { final InputStream inputStream = new ByteArrayInputStream(body); return inputStream2String(inputStream); } /** * 將inputStream裡的資料讀取出來並轉換成字串 * * @param inputStream inputStream * @return String */ private String inputStream2String(InputStream inputStream) { StringBuilder sb = new StringBuilder(); BufferedReader reader = null; try { reader = new BufferedReader(new InputStreamReader(inputStream, Charset.defaultCharset())); String line; while ((line = reader.readLine()) != null) { sb.append(line); } } catch (IOException e) { log.error("", e); throw new RuntimeException(e); } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { log.error("", e); } } } return sb.toString(); } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(getInputStream())); } @Override public ServletInputStream getInputStream() throws IOException { final ByteArrayInputStream inputStream = new ByteArrayInputStream(body); return new ServletInputStream() { @Override public int read() throws IOException { return inputStream.read(); } @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(ReadListener readListener) { } }; } }
除了要寫一個包裝器外,我們還需要在過濾器裡將原生的HttpServletRequest物件替換成我們的RequestWrapper物件,程式碼如下:
package com.example.wrapperdemo.controller.filter; import com.example.wrapperdemo.controller.wrapper.RequestWrapper; import lombok.extern.slf4j.Slf4j; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import java.io.IOException; /** * @author 01 * @program wrapper-demo * @description 替換HttpServletRequest * @create 2018-12-24 21:04 * @since 1.0 **/ @Slf4j public class ReplaceStreamFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { log.info("StreamFilter初始化..."); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { ServletRequest requestWrapper = new RequestWrapper((HttpServletRequest) request); chain.doFilter(requestWrapper, response); } @Override public void destroy() { log.info("StreamFilter銷燬..."); } }
然後我們就可以在攔截器中愉快的獲取json資料也不慌controller層會報錯了:
package com.example.wrapperdemo.controller.interceptor; import com.example.wrapperdemo.controller.wrapper.RequestWrapper; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * @author 01 * @program wrapper-demo * @description 簽名攔截器 * @create 2018-12-24 21:08 * @since 1.0 **/ @Slf4j public class SignatureInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { log.info("[preHandle] executing... request uri is {}", request.getRequestURI()); if (isJson(request)) { // 獲取json字串 String jsonParam = new RequestWrapper(request).getBodyString(); log.info("[preHandle] json資料 : {}", jsonParam); // 驗籤邏輯...略... } return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } /** * 判斷本次請求的資料型別是否為json * * @param request request * @return boolean */ private boolean isJson(HttpServletRequest request) { if (request.getContentType() != null) { return request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE); } return false; } }
編寫完以上的程式碼後,還需要將過濾器和攔截器在配置類中進行註冊才會生效,過濾器配置類程式碼如下:
package com.example.wrapperdemo.config; import com.example.wrapperdemo.controller.filter.ReplaceStreamFilter; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.servlet.Filter; /** * @author 01 * @program wrapper-demo * @description 過濾器配置類 * @create 2018-12-24 21:06 * @since 1.0 **/ @Configuration public class FilterConfig { /** * 註冊過濾器 * * @return FilterRegistrationBean */ @Bean public FilterRegistrationBean someFilterRegistration() { FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setFilter(replaceStreamFilter()); registration.addUrlPatterns("/*"); registration.setName("streamFilter"); return registration; } /** * 例項化StreamFilter * * @return Filter */ @Bean(name = "replaceStreamFilter") public Filter replaceStreamFilter() { return new ReplaceStreamFilter(); } }
攔截器配置類程式碼如下:
package com.example.wrapperdemo.config; import com.example.wrapperdemo.controller.interceptor.SignatureInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * @author 01 * @program wrapper-demo * @description * @create 2018-12-24 21:16 * @since 1.0 **/ @Configuration public class InterceptorConfig implements WebMvcConfigurer { @Bean public SignatureInterceptor getSignatureInterceptor(){ return new SignatureInterceptor(); } /** * 註冊攔截器 * * @param registry registry */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(getSignatureInterceptor()) .addPathPatterns("/**"); } }
接下來我們就可以測試一下在攔截器中讀取了輸入流後在controller層是否還能正常接收資料,首先定義一個實體類,程式碼如下:
package com.example.wrapperdemo.param; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; /** * @author 01 * @program wrapper-demo * @description * @create 2018-12-24 21:11 * @since 1.0 **/ @Data @Builder @NoArgsConstructor @AllArgsConstructor public class UserParam { private String userName; private String phone; private String password; }
然後寫一個簡單的Controller,程式碼如下:
package com.example.wrapperdemo.controller; import com.example.wrapperdemo.param.UserParam; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @author 01 * @program wrapper-demo * @description * @create 2018-12-24 20:47 * @since 1.0 **/ @RestController @RequestMapping("/user") public class DemoController { @PostMapping("/register") public UserParam register(@RequestBody UserParam userParam){ return userParam; } }
啟動專案,請求結果如下,可以看到controller正常接收到資料並返回了:
控制檯輸出如下: