springboot接口如何多次獲取request中的body內(nèi)容
1. 概述
在使用springboot開發(fā)接口時,會將參數(shù)轉(zhuǎn)化為Bean,用來進行參數(shù)的自動校驗。同時也想獲取request中原始body報文進行驗簽(防止報文傳輸過程中被篡改)。
因為通過將bean再轉(zhuǎn)化為字符串后,body里面的報文格式、字段順序會發(fā)生改變,就會導致驗簽失敗。因此只能通過request來獲取body里面的內(nèi)容。
既想接口自動實現(xiàn)參數(shù)校驗,同時又想獲取request中的原始報文,因此我們可以通過在controller中的restful方法中,寫入兩個參數(shù),獲取多次request中的body內(nèi)容。
那么如何實現(xiàn)多次讀取body內(nèi)容了(因為request里的body是以字節(jié)流的方式讀取的,默認情況下讀取一次,字節(jié)流就沒有了),下面就來大致分析一下。
2 接口接收參數(shù)的其他方式
2.1 接收參數(shù)方法一
方法一、
public R list(@RequestBody String rawMsg)
采用上述方式可以直接獲得請求報文中的原始body信息,而且當body是一個json字符串時,rawMsg參數(shù)接口到的body值,不會改變json中key的順序,即與發(fā)送方的body內(nèi)容是保持一致的。這種方式可以用來對報文驗簽,因為被加密的字符串與發(fā)送方是保持一致的。
這種方式可以接受request里面body內(nèi)容的原始格式,保持與發(fā)送方一致。
如下就可以對原始報文進行驗簽操作了
// 用公鑰,對原始報文進行驗簽,在這里如果rawMsg里面是json時,當key的順序改變后,會驗簽失敗, //如此我們可以通過request來獲取body里面的原始報文 boolean verifyResult = SignVerifyUtils.verifySignature(rawMsg, Constant.NPIS_PUBLIC_KEY);
2.2 接收參數(shù)方法二
方法二、
public R list(@RequestBody @Validated ReqBean<ABCReqBean> abcReqBean)
這種接受參數(shù)的方法,可以將request里的json報文,直接轉(zhuǎn)換成對應的bean對象。并且可以用來校驗參數(shù),例如某個字段是必傳的、某個字段的值最大是多少等等。例如
@NotNull(message = "日期字段不允許為空") @Size(min = 8, max = 8, message = "日期字符串的長度必須為 8") private String beginDate;
有沒有一種方法,既能同時利用參數(shù)校驗功能,又能獲取原始body里的內(nèi)容來進行驗簽呢,這時候就可以采用下面的第3中方法。
2.3 接收參數(shù)方法三
@RequestMapping(method = {RequestMethod.POST}, value = "/dataQry") public R list(@RequestBody @Validated ReqBean<ABCReqBean> abcReqBean,HttpServletRequest request){ }
在這里就可以通過將報文轉(zhuǎn)換成abcReqBean對象,并實現(xiàn)接口參數(shù)的自動校驗功能;同時可以利用request獲取原始報文來進行驗簽。
注意:由于在接收參數(shù)時,HttpServletRequest只能讀取一次body內(nèi)容(因為是讀的字節(jié)流,讀完就沒了),因此我們需要需要做特殊處理,
下面來看一種基于SpringBoot來解決HttpServletRequest只能讀取一次的問題。
2.3.1 繼承HttpServletRequestWrapper包裝類,每次讀取body后,再將參數(shù)寫會request
為解決上述多次讀取request中的body內(nèi)容的問題,我們只需要將以下兩個類,放到項目中即可,并通過@Component來注測為spring bean即可
繼承HttpServletRequestWrapper ,實現(xiàn)每次讀取request中的body后,在將內(nèi)容寫回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 { //參數(shù)字節(jié)數(shù)組 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 { /** * 每次調(diào)用此方法時將數(shù)據(jù)流中的數(shù)據(jù)讀取出來,然后再回填到InputStream之中 * 解決通過@RequestBody和@RequestParam(POST方式)讀取一次后控制器拿不到參數(shù)問題 */ 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 將包裝類加入過濾器鏈
回寫參數(shù)的包裝類寫好之后接下來就是加入過濾器鏈之中,如下:
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內(nèi)容問題
一、需求
項目有兩個場景會用到從Request的Body中讀取內(nèi)容。
1、打印請求日志
2、提供Api接口,在api方法執(zhí)行前,從Request Body中讀取參數(shù)進行驗簽,驗簽通過后在執(zhí)行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(網(wǎng)上抄的)
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提交無法獲取參數(shù)
2、獲取RequestBody無問題
四、問題排查
經(jīng)過排查,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到內(nèi)容,讀取結(jié)果為-1
2、v2.2.x、v2.3.x能夠讀取到內(nèi)容
當表單提交時(x-www-form-urlencoded),inputStream讀取一次后后續(xù)不會觸發(fā)wrapper的getInputStream操作,所以Controller無法獲取到參數(shù)。
解決方案
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())); } }
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java輕松實現(xiàn)批量插入或刪除Excel行列操作
在職場生活中,對Excel工作表的行和列進行操作是非常普遍的需求,下面小編就來和大家介紹一下如何在Java中完成批量插入、刪除行和列的操作吧2023-10-10Java Spring-IOC容器與Bean管理之基于注解的方式案例詳解
這篇文章主要介紹了Java Spring-IOC容器與Bean管理之基于注解的方式案例詳解,本篇文章通過簡要的案例,講解了該項技術(shù)的了解與使用,以下就是詳細內(nèi)容,需要的朋友可以參考下2021-08-08Java?C++題解leetcode672燈泡開關(guān)示例
這篇文章主要為大家介紹了Java?C++題解leetcode672燈泡開關(guān)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-09-09Windows下apache ant安裝、環(huán)境變量配置教程
這篇文章主要介紹了Windows下apache ant安裝、環(huán)境變量配置教程,ANT的安裝很簡單,本文同時講解了驗證安裝是否成功的方法和使用方法實例,需要的朋友可以參考下2015-06-06