springboot 解决InputStream只能读取一次的问题
通常在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对象,用于储存请求数据,多次读去请求数据 */ @Slf4jpublic 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 * @return String */ public String getBodyString(final ServletRequest request) { try { return inputStream2String(request.getInputStream()); } catch (IOException e) { log.error("", e); throw new RuntimeException(e); } } /** * 获取请求Body * * @return String */ public String getBodyString() { final InputStream inputStream = new ByteArrayInputStream(body); return inputStream2String(inputStream); } /** * 将inputStream里的数据读取出来并转换成字符串 * * @param inputStream inputStream * @return String */ private String inputStream2String(InputStream inputStream) { StringBuilder sb = new StringBuilder(); BufferedReader reader = null; try { reader = new BufferedReader(new InputStreamReader(inputStream, Charset.defaultCharset())); String line; while ((line = reader.readLine()) != null) { sb.append(line); } } catch (IOException e) { log.error("", e); throw new RuntimeException(e); } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { log.error("", e); } } } return sb.toString(); } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(getInputStream())); } @Override public ServletInputStream getInputStream() throws IOException { final ByteArrayInputStream inputStream = new ByteArrayInputStream(body); return new ServletInputStream() { //重写read方法@Override public int read() throws IOException { return inputStream.read(); } @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(ReadListener readListener) { } }; } }
- 过滤器
在过滤器里将原生的HttpServletRequest对象替换成我们的RequestWrapper对象
package com.example.filter; import com.example.request.RequestWrapper; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import java.io.IOException; /** * 替换HttpServletRequest */ @Component @Slf4jpublic class CustomizeFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { log.info("CustomizeFilter 初始化..."); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { log.info("CustomizeFilter 执行过滤器..."); ServletRequest requestWrapper = null; if (request instanceof HttpServletRequest) { //如果数据类型是json格式的数据,则使用包装request容器转换 if (isJson((HttpServletRequest) request)) { requestWrapper = new RequestWrapper((HttpServletRequest) request); } } if (requestWrapper == null) { chain.doFilter(request, response); } else { //防止流读取一次就没有了,将流传递下去 chain.doFilter(requestWrapper, response); } } @Override public void destroy() { log.info("CustomizeFilter 销毁..."); } /** * 判断本次请求的数据类型是否为json * @param request request * @return boolean */ private boolean isJson(HttpServletRequest request) { if (request.getContentType() != null) { return request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE); } return false; } }
- 拦截器
在拦截器中去读取json格式数据,如果不用包装request容器,则request传递到controller层使用@RequestBody注解解析json时会报错,因此在拦截器中想要读取请求body,需要使用到request封装容器。
package com.example.interceptor; import com.example.request.RequestWrapper; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * @program 拦截器读取json数据做接口校验使用 **/ @Component @Slf4jpublic class SignatureInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { log.info("preHandle() 执行... request uri is {}", request.getRequestURI()); if (isJson(request)) { // 获取json字符串 String jsonParam = new RequestWrapper(request).getBodyString(); log.info("[preHandle] json数据 : {}", jsonParam); // 验签逻辑...略... } return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { log.info("postHandle() 执行 .... "); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { log.info("afterCompletion() 执行 .... "); } /** * 判断本次请求的数据类型是否为json * * @param request request * @return boolean */ private boolean isJson(HttpServletRequest request) { if (request.getContentType() != null) { return request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE); } return false; } }
- 配置拦截器
package com.example.config; import com.example.interceptor.SignatureInterceptor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Slf4j @Configurationpublic class InterceptorConfig implements WebMvcConfigurer { @Autowired private SignatureInterceptor signatureInterceptor; /** * 注册拦截器 * * @param registry registry */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(signatureInterceptor).addPathPatterns("/**"); } }
以上完成了,request的二次封装,但是如果还想读取响应的数据,则需要定义ResponseBodyAdvice
- 自定义ResponseBodyAdvice
package com.example.advice; import com.example.request.RequestWrapper; import lombok.extern.slf4j.Slf4j; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.lang.Nullable; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; import javax.servlet.http.HttpServletRequest; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.Charset; /** * 此类可以记录请求和响应日志 * * */ @Slf4j @ControllerAdvicepublic class InterceptResponse implements ResponseBodyAdvice<Object> { @Override public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) { return true; } @Nullable @Override public Object beforeBodyWrite(@Nullable Object body, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest request, ServerHttpResponse serverHttpResponse) { //1.request 读取请求日志 InputStream inputStream = null; try { inputStream = request.getBody(); } catch (Exception e) { log.error("beforeBodyWrite()--->>>request.getBody()", e); } String requestParam = inputStream2String(inputStream); log.info("请求日志:{}", requestParam); //此处的 body 对象是controller层自定义的返回值类型,具体根据自己需求修改即可 log.info("响应日志:{}", body); return body; } /** * 将inputStream里的数据读取出来并转换成字符串 * * @param inputStream inputStream * @return String */ private String inputStream2String(InputStream inputStream) { StringBuilder sb = new StringBuilder(); BufferedReader reader = null; try { reader = new BufferedReader(new InputStreamReader(inputStream, Charset.defaultCharset())); String line; while ((line = reader.readLine()) != null) { sb.append(line); } } catch (IOException e) { log.error("inputStream2String()", e); throw new RuntimeException(e); } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { log.error("", e); } } } return sb.toString(); } /** * 判断本次请求的数据类型是否为json * * @param request request * @return boolean */ private boolean isJson(HttpServletRequest request) { if (request.getContentType() != null) { return request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE); } return false; } }