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