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。

推薦閱讀: