通常对安全性有要求得接口都会对请求参数做一些签名验证,而我们一般会把验签得逻辑统一放到过滤器或拦截器里,这样就不用每个接口都去重复编写验签得逻辑。
在一个项目中会有很多得接口,而不同得接口可能接收不同类型得数据,例如表单数据和json数据,表单数据还好说,调用request得getParameterMap就能全部取出来。而json数据就有些麻烦了,因为json数据放在body中,我们需要通过request得输入流去读取。
但问题在于request得输入流只能读取一次不能重复读取,所以我们在过滤器或拦截器里读取了request得输入流之后,请求走到controller层时就会报错。而感谢得目得就是介绍如何解决在这种场景下遇到HttpServletRequest得输入流只能读取一次得问题。
注:感谢代码基于SpringBoot框架
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,可以看到该类没有重写mark(),reset()以及markSupported()方法:
综上,InputStream默认不实现reset得相关方法,而ServletInputStream也没有重写reset得相关方法,这样就无法重复读取流,这就是我们从request对象中获取得输入流就只能读取一次得原因。
使用HttpServletRequestWrapper + Filter解决输入流不能重复读取问题既然ServletInputStream不支持重新读写,那么为什么不把流读出来后用容器存储起来,后面就可以多次利用了。那么问题就来了,要如何存储这个流呢?
所幸JavaEE提供了一个 HttpServletRequestWrapper类,从类名也可以知道它是一个http请求包装器,其基于装饰者模式实现了HttpServletRequest界面,部分源码如下:
从上图中得部分源码可以看到,该类并没有真正去实现HttpServletRequest得方法,而只是在方法内又去调用HttpServletRequest得方法,所以我们可以通过继承该类并实现想要重新定义得方法以达到包装原生HttpServletRequest对象得目得。
首先我们要定义一个容器,将输入流里面得数据存储到这个容器里,这个容器可以是数组或集合。然后我们重写getInputStream方法,每次都从这个容器里读数据,这样我们得输入流就可以读取任意次了。
具体得实现代码如下:
package com.example.wrapperdemo.controller.wrapper;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;等Slf4jpublic class RequestWrapper extends HttpServletRequestWrapper { private final byte[] body; public RequestWrapper(HttpServletRequest request) throws IOException { super(request); // 将body数据存储起来 String bodyStr = getBodyString(request); body = bodyStr.getBytes(Charset.defaultCharset()); } public String getBodyString(final ServletRequest request) { try { return inputStream2String(request.getInputStream()); } catch (IOException e) { log.error("", e); throw new RuntimeException(e); } } public String getBodyString() { final InputStream inputStream = new ByteArrayInputStream(body); return inputStream2String(inputStream); } 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() { 等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.wrapperdemo.controller.filter;import com.example.wrapperdemo.controller.wrapper.RequestWrapper;import lombok.extern.slf4j.Slf4j;import javax.servlet.*;import javax.servlet.http.HttpServletRequest;import java.io.IOException;等Slf4jpublic class ReplaceStreamFilter implements Filter { 等Override public void init(FilterConfig filterConfig) throws ServletException { log.info("StreamFilter初始化..."); } 等Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { ServletRequest requestWrapper = new RequestWrapper((HttpServletRequest) request); chain.doFilter(requestWrapper, response); } 等Override public void destroy() { log.info("StreamFilter销毁..."); }}
复制
然后我们就可以在拦截器中愉快得获取json数据也不慌controller层会报错了:
package com.example.wrapperdemo.controller.interceptor;import com.example.wrapperdemo.controller.wrapper.RequestWrapper;import lombok.extern.slf4j.Slf4j;import org.springframework.http.MediaType;import org.springframework.web.servlet.HandlerInterceptor;import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;等Slf4jpublic class SignatureInterceptor implements HandlerInterceptor { 等Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { log.info("[preHandle] executing... 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 { } 等Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } 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.wrapperdemo.config;import com.example.wrapperdemo.controller.filter.ReplaceStreamFilter;import org.springframework.boot.web.servlet.FilterRegistrationBean;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import javax.servlet.Filter;等Configurationpublic class FilterConfig { 等Bean public FilterRegistrationBean someFilterRegistration() { FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setFilter(replaceStreamFilter()); registration.addUrlPatterns(" 等Bean(name = "replaceStreamFilter") public Filter replaceStreamFilter() { return new ReplaceStreamFilter(); }}
复制
拦截器配置类代码如下:
package com.example.wrapperdemo.config;import com.example.wrapperdemo.controller.interceptor.SignatureInterceptor;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;等Configurationpublic class InterceptorConfig implements WebMvcConfigurer { 等Bean public SignatureInterceptor getSignatureInterceptor(){ return new SignatureInterceptor(); } 等Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(getSignatureInterceptor()) .addPathPatterns("等Data等Builder等NoArgsConstructor等AllArgsConstructorpublic class UserParam { private String userName; private String phone; private String password;}
复制
然后写一个简单得Controller,代码如下:
package com.example.wrapperdemo.controller;import com.example.wrapperdemo.param.UserParam;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;等RestController等RequestMapping("/user")public class DemoController { 等PostMapping("/register") public UserParam register(等RequestBody UserParam userParam){ return userParam; }}
复制
启动项目,请求结果如下,可以看到controller正常接收到数据并返回了:
控制台输出如下:
感谢分享cloud.tencent感谢原创分享者/developer/article/1702246