通常在springboot应用中,对接口的安全性有要求时都会对请求参数做一些签名验证,这些验证逻辑一般都是统一放到过滤器或拦截器里,这样就不用每个接口都去重复编写验签的逻辑。对于接口有可能接收不同类型的数据,对于表单数据来说,只要调用request的getParameterMap就能全部取出来。对于json数据来说,需要通过request的输入流去读取。
但问题在于request的输入流只能读取一次不能重复读取,所以我们在过滤器或拦截器里读取了request的输入流之后,请求走到controller层时就会报错。而本文的目的就是介绍如何解决在这种场景下遇到HttpServletRequest的输入流只能读取一次的问题。
HttpServletRequest的输入流只能读取一次的原因
HttpServletRequest 对象中调用getInputStream()方法获取输入流时得到的是一个InputStream对象,而实际类型是ServletInputStream,它继承于InputStream。InputStream的read()方法内部有一个postion,标志当前流被读取到的位置,每读取一次,该标志就会移动一次,如果读到最后,read()会返回-1,表示已经读取完了。如果想要重新读取则需要调用reset()方法,position就会移动到上次调用mark的位置,mark默认是0,所以就能从头再读了。调用reset()方法的前提是已经重写了reset()方法,当然能否reset也是有条件的,它取决于markSupported()方法是否返回true。但是InputStream默认不实现reset(),并且markSupported()默认也是返回false。
查看源码截图如下:
ServletInputStream 查看源码得知,ServletInputStream也没有重写reset的相关方法,这样就无法重复读取流,这就是我们从request对象中获取的输入流就只能读取一次的原因。
解决InputStream不能重复读取问题
我们的JavaEE提供了一个 HttpServletRequestWrapper类,HttpServletRequestWrapper集成ServletRequestWrapper并且实现HttpServletRequest接口。在查看源码得知,该类并没有真正去实现HttpServletRequest的方法,而只是在方法内又去调用HttpServletRequest的方法,因此我们可以通过继承该类并实现想要重新定义的方法以达到包装原生HttpServletRequest对象的目的。
具体的实现代码如下:
- 定义一个request容器,包装原来框架的request对象,用于储存请求数据,多次读去请求数据
package com.example.request;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.Charset;
/**
* 定义一个request容器,包装原来框架的request对象,用于储存请求数据,多次读去请求数据
*/
@Slf4j
public class CustomizeRequestWrapper extends HttpServletRequestWrapper {
/**
* 存储body数据的容器
*/
private final byte[] body;
public RequestWrapper(HttpServletRequest request) throws IOException {
super(request);
// 将body数据存储起来
String bodyStr = getBodyString(request);
body = bodyStr.getBytes(Charset.defaultCharset());
}
/**
* 获取请求Body
*
* @param request request
*