springboot接口如何多次獲取request中的body內容
1. 概述
在使用springboot開發接口時,會將參數轉化為Bean,用來進行參數的自動校驗。同時也想獲取request中原始body報文進行驗簽(防止報文傳輸過程中被篡改)。
因為通過將bean再轉化為字符串後,body裡面的報文格式、字段順序會發生改變,就會導致驗簽失敗。因此隻能通過request來獲取body裡面的內容。
既想接口自動實現參數校驗,同時又想獲取request中的原始報文,因此我們可以通過在controller中的restful方法中,寫入兩個參數,獲取多次request中的body內容。
那麼如何實現多次讀取body內容瞭(因為request裡的body是以字節流的方式讀取的,默認情況下讀取一次,字節流就沒有瞭),下面就來大致分析一下。
2 接口接收參數的其他方式
2.1 接收參數方法一
方法一、
public R list(@RequestBody String rawMsg)
采用上述方式可以直接獲得請求報文中的原始body信息,而且當body是一個json字符串時,rawMsg參數接口到的body值,不會改變json中key的順序,即與發送方的body內容是保持一致的。這種方式可以用來對報文驗簽,因為被加密的字符串與發送方是保持一致的。
這種方式可以接受request裡面body內容的原始格式,保持與發送方一致。
如下就可以對原始報文進行驗簽操作瞭
// 用公鑰,對原始報文進行驗簽,在這裡如果rawMsg裡面是json時,當key的順序改變後,會驗簽失敗, //如此我們可以通過request來獲取body裡面的原始報文 boolean verifyResult = SignVerifyUtils.verifySignature(rawMsg, Constant.NPIS_PUBLIC_KEY);
2.2 接收參數方法二
方法二、
public R list(@RequestBody @Validated ReqBean<ABCReqBean> abcReqBean)
這種接受參數的方法,可以將request裡的json報文,直接轉換成對應的bean對象。並且可以用來校驗參數,例如某個字段是必傳的、某個字段的值最大是多少等等。例如
@NotNull(message = "日期字段不允許為空") @Size(min = 8, max = 8, message = "日期字符串的長度必須為 8") private String beginDate;
有沒有一種方法,既能同時利用參數校驗功能,又能獲取原始body裡的內容來進行驗簽呢,這時候就可以采用下面的第3中方法。
2.3 接收參數方法三
@RequestMapping(method = {RequestMethod.POST}, value = "/dataQry") public R list(@RequestBody @Validated ReqBean<ABCReqBean> abcReqBean,HttpServletRequest request){ }
在這裡就可以通過將報文轉換成abcReqBean對象,並實現接口參數的自動校驗功能;同時可以利用request獲取原始報文來進行驗簽。
註意:由於在接收參數時,HttpServletRequest隻能讀取一次body內容(因為是讀的字節流,讀完就沒瞭),因此我們需要需要做特殊處理,
下面來看一種基於SpringBoot來解決HttpServletRequest隻能讀取一次的問題。
2.3.1 繼承HttpServletRequestWrapper包裝類,每次讀取body後,再將參數寫會request
為解決上述多次讀取request中的body內容的問題,我們隻需要將以下兩個類,放到項目中即可,並通過@Component來註測為spring bean即可
繼承HttpServletRequestWrapper ,實現每次讀取request中的body後,在將內容寫回request。
package com.abcd.config; import org.apache.commons.io.IOUtils; import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.*; /** * @author: */ public class RequestWrapper extends HttpServletRequestWrapper { //參數字節數組 private byte[] requestBody; //Http請求對象 private HttpServletRequest request; public RequestWrapper(HttpServletRequest request) throws IOException { super(request); this.request = request; } /** * @return * @throws IOException */ @Override public ServletInputStream getInputStream() throws IOException { /** * 每次調用此方法時將數據流中的數據讀取出來,然後再回填到InputStream之中 * 解決通過@RequestBody和@RequestParam(POST方式)讀取一次後控制器拿不到參數問題 */ if (null == this.requestBody) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); IOUtils.copy(request.getInputStream(), baos); this.requestBody = baos.toByteArray(); } 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 listener) { } @Override public int read() { return bais.read(); } }; } public byte[] getRequestBody() { return requestBody; } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(this.getInputStream())); } }
2.3.2 將包裝類加入過濾器鏈
回寫參數的包裝類寫好之後接下來就是加入過濾器鏈之中,如下:
package com.abcd.config; import org.springframework.stereotype.Component; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import java.io.IOException; /** * @author: */ @Component @WebFilter(filterName = "channelFilter", urlPatterns = {"/*"}) public class ChannelFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { try { ServletRequest requestWrapper = null; if (request instanceof HttpServletRequest) { requestWrapper = new RequestWrapper((HttpServletRequest) request); } if (requestWrapper == null) { chain.doFilter(request, response); } else { chain.doFilter(requestWrapper, response); } } catch (IOException e) { e.printStackTrace(); } catch (ServletException e) { e.printStackTrace(); } } @Override public void destroy() { } }
解決springboot v2.2以上重復讀取request body內容問題
一、需求
項目有兩個場景會用到從Request的Body中讀取內容。
1、打印請求日志
2、提供Api接口,在api方法執行前,從Request Body中讀取參數進行驗簽,驗簽通過後在執行api方法
二、解決方案
2.1 自定義RequestWrapper
public class MyRequestWrapper extends HttpServletRequestWrapper { private final String body; public MyRequestWrapper(HttpServletRequest request) throws IOException { super(request); this.body = RequestReadUtils.read(request); } public String getBody() { return body; } @Override public ServletInputStream getInputStream() throws IOException { final ByteArrayInputStream bais = new ByteArrayInputStream(body.getBytes()); return new ServletInputStream() { ...略 }; } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(this.getInputStream())); } }
RequestReadUtils(網上抄的)
private static final int BUFFER_SIZE = 1024 * 8; public static String read(HttpServletRequest request) throws IOException { BufferedReader bufferedReader = request.getReader(); for (Enumeration<String> iterator = request.getHeaderNames(); iterator.hasMoreElements();) { String type = iterator.nextElement(); System.out.println(type+" = "+request.getHeader(type)); } System.out.println(); StringWriter writer = new StringWriter(); write(bufferedReader,writer); return writer.getBuffer().toString(); } public static long write(Reader reader,Writer writer) throws IOException { return write(reader, writer, BUFFER_SIZE); } public static long write(Reader reader, Writer writer, int bufferSize) throws IOException { int read; long total = 0; char[] buf = new char[bufferSize]; while( ( read = reader.read(buf) ) != -1 ) { writer.write(buf, 0, read); total += read; } return total; }
2.2 定義Filter
@WebFilter public class TestFilter implements Filter{ @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain){ HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; MyRequestWrapper wrapper = WebUtils.getNativeRequest(request, MyRequestWrapper.class); chain.doFilter(wrapper == null ? new MyRequestWrapper(request) :wrapper,servletRequest); } }
三、遇到問題
使用的SpringBoot v2.1.x版本
1、Form提交無問題
2、獲取RequestBody無問題
使用SpringBoot v2.2.0以上版本(包括v2.3.x)
1、Form提交無法獲取參數
2、獲取RequestBody無問題
四、問題排查
經過排查,v2.2.x對比v2.1.x的不同在於一下代碼差異:
BufferedReader bufferedReader = request.getReader(); ----------------- char[] buf = new char[bufferSize]; while( ( read = reader.read(buf) ) != -1 ) { writer.write(buf, 0, read); total += read; }
當表單提交時
1、v2.1.x無法read到內容,讀取結果為-1
2、v2.2.x、v2.3.x能夠讀取到內容
當表單提交時(x-www-form-urlencoded),inputStream讀取一次後後續不會觸發wrapper的getInputStream操作,所以Controller無法獲取到參數。
解決方案
MyRequestWrapper改造
public class MyRequestWrapper extends HttpServletRequestWrapper { private final String body; public MyRequestWrapper(HttpServletRequest request) throws IOException { super(request); this.body = getBodyString(request); } public String getBody() { return body; } public String getBodyString(final HttpServletRequest request) throws IOException { String contentType = request.getContentType(); String bodyString = ""; StringBuilder sb = new StringBuilder(); if (StringUtils.isNotBlank(contentType) && (contentType.contains("multipart/form-data") || contentType.contains("x-www-form-urlencoded"))) { Map<String, String[]> parameterMap = request.getParameterMap(); for (Map.Entry<String, String[]> next : parameterMap.entrySet()) { String[] values = next.getValue(); String value = null; if (values != null) { if (values.length == 1) { value = values[0]; } else { value = Arrays.toString(values); } } sb.append(next.getKey()).append("=").append(value).append("&"); } if (sb.length() > 0) { bodyString = sb.toString().substring(0, sb.toString().length() - 1); } return bodyString; } else { return IOUtils.toString(request.getInputStream()); } } @Override public ServletInputStream getInputStream() throws IOException { final ByteArrayInputStream bais = new ByteArrayInputStream(body.getBytes()); return new ServletInputStream() { @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return false; } @Override public int read() { return bais.read(); } @Override public void setReadListener(ReadListener readListener) { } }; } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(this.getInputStream())); } }
以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。
推薦閱讀:
- 使用ServletInputStream在攔截器或過濾器中應用後重寫
- 在攔截器中讀取request參數,解決在controller中無法二次讀取的問題
- 使用@RequestBody傳遞多個不同對象方式
- SpringBoot通過AOP與註解實現入參校驗詳情
- Request的包裝類HttpServletRequestWrapper的使用說明