解决 HttpServletRequest 的输入流只能读取一次的问题
通常对安全性有要求的接口都会对请求参数做一些签名验证,而我们一般会把验签的逻辑统一放到过滤器或拦截器里,这样就不用每个接口都去重复编写验签的逻辑。
在一个项目中会有很多的接口,而不同的接口可能接收不同类型的数据,例如表单数据和json数据,表单数据还好说,调用request的getParameterMap就能全部取出来。而json数据就有些麻烦了,因为json数据放在body中,我们需要通过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。
ContentCachingRequestWrapper
Spring Boot 提供了一个简单的封装器 ContentCachingRequestWrapper,从源码上看这个封装器并不实用,没有封装http的底层流ServletInputStream信息,导致使用@RequestParam,@RequestBody等使用底层流构建的逻辑依然无用,只能硬生生的使用。
在 Web filter 的 doFilter 方法里,创建 ContentCachingRequestWrapper 对 request做包装。
1
2
3
4
5
6
7
8
9
10
|
@Component
public class CachingRequestBodyFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest currentRequest = (HttpServletRequest) servletRequest;
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(currentRequest);
chain.doFilter(wrappedRequest, servletResponse);
}
}
|
接下来我们就可以在 Controller 里调用 ContentCachingRequestWrapper 的方法获取 request body。示例如下:
1
2
3
4
5
6
7
8
9
|
@RestController
public class HelloController {
@PostMapping("/hello")
public String hello(@RequestBody String id, HttpServletRequest request) {
ContentCachingRequestWrapper requestWrapper = (ContentCachingRequestWrapper) request;
String requestBody = new String(requestWrapper.getContentAsByteArray());
return "body: " + requestBody;
}
}
|
所幸JavaEE提供了一个 HttpServletRequestWrapper类,从类名也可以知道它是一个http请求包装器,其基于装饰者模式实现了HttpServletRequest界面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
|
/**
* @description 包装 HttpServletRequest,目的是让其输入流可重复读
**/
@Slf4j
public class CachedRequestWrapper extends HttpServletRequestWrapper {
/** 存储body数据的容器 */
private final byte[] body;
public CachedRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
body = ByteStreams.toByteArray(request.getInputStream());
}
/**
* 获取请求Body
*
* @return String
*/
public String getBodyString() {
return new String(body, StandardCharsets.UTF_8);
}
@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) {
// 空
}
};
}
}
public class ReplaceStreamFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
ServletRequest requestWrapper = new RequestWrapper((HttpServletRequest) request);
chain.doFilter(requestWrapper, response);
}
}
@Configuration
public class FilterConfig {
/**
* 注册过滤器
*
* @return FilterRegistrationBean
*/
@Bean
public FilterRegistrationBean someFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(replaceStreamFilter());
registration.addUrlPatterns("/*");
registration.setName("streamFilter");
return registration;
}
/**
* 实例化StreamFilter
*
* @return Filter
*/
@Bean(name = "replaceStreamFilter")
public Filter replaceStreamFilter() {
return new ReplaceStreamFilter();
}
}
|
拦截器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
public class SignatureInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if (isJson(request)) {
// 获取json字符串
String jsonParam = new CachedRequestWrapper(request).getBodyString();
// 验签逻辑...略...
}
return true;
}
private boolean isJson(HttpServletRequest request) {
if (request.getContentType() != null) {
return request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE);
}
return false;
}
}
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Bean
public SignatureInterceptor getSignatureInterceptor() {
return new SignatureInterceptor();
}
/**
* 注册拦截器
*
* @param registry registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(getSignatureInterceptor()).addPathPatterns("/**");
}
}
|