How does the springboot interface retrieve body content in request multiple times

  • 2021-10-13 07:30:26
  • OfStack

1. Overview

When using springboot development interface, the parameters will be converted to Bean, which is used for automatic verification of parameters. At the same time, we also want to obtain the original body message in request for verification (to prevent the message from being tampered with during transmission).

Because after bean is converted into a string, the message format and field order in body will change, which will lead to the failure of signature verification. Therefore, the contents of body can only be obtained through request.

We want to automatically check the parameters of the interface and obtain the original message in request at the same time, so we can write two parameters in restful method in controller to obtain body content in request for many times.

So how to read the contents of body many times (because body in request is read in the way of byte stream, read once by default, and the byte stream is gone), let's roughly analyze 1 below.

Other ways for the 2 interface to receive parameters

2.1 Receiving Parameters Method 1

Method 1.


public R list(@RequestBody String rawMsg)

The original body information in the request message can be obtained directly by adopting the above method, and when body is an json string, the body value to which the rawMsg parameter interface will not change the order of key in json, that is, it is consistent with the content of body of the sender. This method can be used to check the message, because the encrypted string is consistent with the sender.

In this way, the original format of body content in request can be accepted, and the original format of request content can be maintained with sender 1.

You can check and sign the original message as follows


//  Use the public key to check and sign the original message, where if rawMsg Inside is json When, when key After the order is changed, the signature will fail. 
// So that we can pass request To get body The original message inside 
boolean verifyResult = SignVerifyUtils.verifySignature(rawMsg, Constant.NPIS_PUBLIC_KEY);

2.2 Method of receiving parameters 2

Method 2.


public R list(@RequestBody @Validated ReqBean<ABCReqBean> abcReqBean)

This method of accepting parameters can directly convert the json message in request into the corresponding bean object. It can also be used to verify parameters, such as a certain field is required, the maximum value of a certain field, and so on. For example


    @NotNull(message = " Date field is not allowed to be blank ")
    @Size(min = 8, max = 8, message = " The length of the date string must be  8")
    private String beginDate;

Is there a method that can use the parameter verification function at the same time and obtain the contents of the original body for verification? At this time, the following method 3 can be adopted.

2.3 Method of receiving parameters 3


@RequestMapping(method = {RequestMethod.POST}, value = "/dataQry") 
public R list(@RequestBody @Validated ReqBean<ABCReqBean> abcReqBean,HttpServletRequest request){
}

Here, we can convert the message into abcReqBean object and realize the automatic verification function of interface parameters; At the same time, request can be used to obtain the original message for signature verification.

Note: Since HttpServletRequest can only read the contents of body once when receiving parameters (because it is a read byte stream, it will disappear after reading), we need to do special processing.

Let's look at a solution based on SpringBoot to solve the problem that HttpServletRequest can only be read once.

2.3. 1 Inherit the HttpServletRequestWrapper wrapper class and write the parameters to request after reading body each time

In order to solve the problem of reading body content in request many times, we only need to put the following two classes into the project and test them as spring bean through @ Component

Inherit HttpServletRequestWrapper, and write back request after reading body in request every time.


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 {
    // Array of parameter bytes 
    private byte[] requestBody;
    //Http Request object 
    private HttpServletRequest request; 
    public RequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        this.request = request;
    }
 
    /**
     * @return
     * @throws IOException
     */
    @Override
    public ServletInputStream getInputStream() throws IOException {
        /**
         *  Each time this method is called, the data in the data flow is read out, and then backfilled into the InputStream In 
         *  Resolve and pass @RequestBody And @RequestParam ( POST Mode) read 1 The problem that the secondary controller can't get the parameters 
         */
        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 Add wrapper class to filter chain

After writing the wrapper class of the write-back parameter, the next step is to add it to the filter chain, as follows:


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() {
    } 
}

Solve the problem of repeatedly reading request body contents above springboot v2.2

1. Requirements

There are two scenarios for the project that will use reading content from Body of Request.

1. Print the request log

2. The Api interface is provided. Before the api method is executed, the parameters are read from the Request Body for verification. After the verification is passed, the api method is executed

2. Solutions

2.1 Customize 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() {
			... Slightly 
		};
	}
	@Override
	public BufferedReader getReader() throws IOException {
		return new BufferedReader(new InputStreamReader(this.getInputStream()));
	}
}

RequestReadUtils (copied online)


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 Defining 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);
 }
}

STEP 3 Encounter problems

SpringBoot v 2.1. x version used

1. Form submission is no problem

2. There is no problem in obtaining RequestBody

Use SpringBoot v 2.2. 0 and above (including v 2.3. x)

1. Form commit could not get parameters

2. There is no problem in getting RequestBody

4. Problem troubleshooting

After investigation, the difference between v2.2. x and v2.1. x lies in the code difference under 1:


//  Use the public key to check and sign the original message, where if rawMsg Inside is json When, when key After the order is changed, the signature will fail. 
// So that we can pass request To get body The original message inside 
boolean verifyResult = SignVerifyUtils.verifySignature(rawMsg, Constant.NPIS_PUBLIC_KEY);
0

When the form is submitted

1. v2.1. x cannot read to the content, and the read result is-1

2. v2.2. x, v2.3. x can read the content

When the form is submitted (x-www-form-urlencoded), the getInputStream operation of wrapper will not be triggered after inputStream is read once, so Controller cannot get parameters.

Solutions

Transformation of MyRequestWrapper


//  Use the public key to check and sign the original message, where if rawMsg Inside is json When, when key After the order is changed, the signature will fail. 
// So that we can pass request To get body The original message inside 
boolean verifyResult = SignVerifyUtils.verifySignature(rawMsg, Constant.NPIS_PUBLIC_KEY);
1

Related articles: