在使用AOP编程的时候,经常碰到需要多次获取整个请求的body的情况。例如:典型场景下我们要在AOP切面中做日志记录或权限校验,此时需要调用request.getInputStream
获取输入流,从而读取整个请求的消息体。但是这通常会触发一个异常:java.lang.IllegalStateException: getInputStream() can't be called after getReader()
。
出现这个问题的原因是默认的HttpServletRequest
对象中的getInputStream
,getReader
函数式只允许调用一次。在一次请求中,除了我们在切面中调用getInputStream
之外,Spring MVC
框架在进行参数转换的时候还需要调用getInputStream
方法读取整个请求的消息体,然后转回为请求参数,这违背了只调用一次的原则,从而触发了以异常,
为了解决这个问题,我们可以引入HttpServletRequestWrapper
这个对象。这个类封装了HttpServletRequest
的行为,我们可以继承这个类,从而使用一个新类模拟原始HttpServletRequest
的行为。然后使用过滤器(filter)将原始的HttpServletRequest
对象替换为HttpServletRequestWrapper
对象。
最近在项目中有需求为API请求增加参数签名校验,使用了AOP切面功能,因此碰到了上面的问题:参数校验切面中需要在读取整个请求报文,然后对报文进行hmac算法从而计算签名值。下面说一下具体的解决办法,以代码为主。
1. 相关代码
1.1 RequestWrapper
RequestWrapper
继承了HttpServletRequestWrapper
,初始化的时候读取Request的整个请求体。然后重载了getInputStream
和getReader
方法,这两个方法会从类变量body
中读取内容。
1public class RequestWrapper extends HttpServletRequestWrapper {
2 private final String body;
3
4 public RequestWrapper(HttpServletRequest request) throws IOException
5 {
6 //So that other request method behave just like before
7 super(request);
8
9 StringBuilder stringBuilder = new StringBuilder();
10 BufferedReader bufferedReader = null;
11 try {
12 InputStream inputStream = request.getInputStream();
13 if (inputStream != null) {
14 bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
15 char[] charBuffer = new char[128];
16 int bytesRead = -1;
17 while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
18 stringBuilder.append(charBuffer, 0, bytesRead);
19 }
20 } else {
21 stringBuilder.append("");
22 }
23 } catch (IOException ex) {
24 throw ex;
25 } finally {
26 if (bufferedReader != null) {
27 try {
28 bufferedReader.close();
29 } catch (IOException ex) {
30 throw ex;
31 }
32 }
33 }
34 //Store request pody content in 'body' variable
35 body = stringBuilder.toString();
36 }
37
38 @Override
39 public ServletInputStream getInputStream() throws IOException {
40 final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
41 ServletInputStream servletInputStream = new ServletInputStream() {
42 @Override
43 public boolean isFinished() {
44 return false;
45 }
46
47 @Override
48 public boolean isReady() {
49 return true;
50 }
51
52 @Override
53 public void setReadListener(ReadListener readListener) {
54 throw new UnsupportedOperationException();
55 }
56
57 public int read() throws IOException {
58 return byteArrayInputStream.read();
59 }
60 };
61 return servletInputStream;
62 }
63
64 @Override
65 public BufferedReader getReader() throws IOException {
66 return new BufferedReader(new InputStreamReader(this.getInputStream()));
67 }
68
69 //Use this method to read the request body N times
70 public String getBody() {
71 return this.body;
72 }
73}
1.2 RequestWrapperFilter
RequestWrapperFilter
是一个过滤器,它完成将普通的HttpServletRequest
转化为RequestWrapper
对象的工作。使用时需要使用该filter
过滤需要添加校验的url。
1public class RequestWrapperFilter implements Filter {
2 private FilterConfig filterConfig = null;
3
4 @Override
5 public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
6 servletRequest = new RequestWrapper((HttpServletRequest) servletRequest);
7 //Read request.getBody() as many time you need
8 filterChain.doFilter(servletRequest, servletResponse);
9 }
10
11 @Override
12 public void init(FilterConfig filterConfiguration) throws ServletException {
13 this.filterConfig = filterConfiguration;
14 }
15
16 @Override
17 public void destroy() {
18 this.filterConfig = null;
19 }
20}
1.3 Configuration
在配置类中需要使用FilterRegisterationBean
对过滤器进行配置。这里配置过滤url为:/api/dpp/*
。
1 @Bean(name="apiAuthFilter")
2 public RequestWrapperFilter apiAuthFilter() {
3 return new RequestWrapperFilter();
4 }
5
6 @Bean(name="apiAuthBean")
7 public FilterRegistrationBean apiAuthBean() {
8 FilterRegistrationBean registration = new FilterRegistrationBean(apiAuthFilter());
9 registration.setOrder(1);
10 registration.addUrlPatterns("/api/dpp/*");
11 return registration;
12 }
1.4 ApiAuthAspect
ApiAuthAspect
完成签名的校验功能。在该类中使用了Spring的RequestContextHolder
类获取当前请求对应的HttpServletRequest
。注意:这里获取到的对象中实际包含的对象是RequestWrapper
对象。
因为是已经重新包装过的RequestWrapper
对象,所以可以再次调用getReader
,getInputStream
方法以获取整个消息体。
需要注意的是:获取到的HttpServletRequest
对象无法直接进行类型转换,转换为RequestWrapper
,类型不匹配。猜测是容器封装了多次的原因。有时间再仔细研究。
1/**
2 * 获取当前请求的HttpServletRequest对象
3 * @return
4 */
5 protected HttpServletRequest getHttpServletRequest() {
6 return ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
7 }
8
9 protected String getRequestBody(HttpServletRequest request) {
10 StringBuffer sb = new StringBuffer();
11 BufferedReader bufferedReader = null;
12 String content = "";
13
14 try {
15 //InputStream inputStream = request.getInputStream();
16 //inputStream.available();
17 //if (inputStream != null) {
18 bufferedReader = request.getReader() ; //new BufferedReader(new InputStreamReader(inputStream));
19 char[] charBuffer = new char[128];
20 int bytesRead;
21 while ( (bytesRead = bufferedReader.read(charBuffer)) != -1 ) {
22 sb.append(charBuffer, 0, bytesRead);
23 }
24 //} else {
25 // sb.append("");
26 //}
27
28 } catch (IOException ex) {
29 logger.error("Failed to getInputStream.", ex);
30 } finally {
31 if (bufferedReader != null) {
32 try {
33 bufferedReader.close();
34 } catch (IOException ex) {
35 logger.error("Failed to getInputStream.", ex);
36 }
37 }
38 }
39
40 return sb.toString();
41 }
42
43 public Object apiAuthAround(ProceedingJoinPoint joinPoint) throws Throwable {
44 Object request = joinPoint.getArgs()[0];
45 MethodSignature signature = ((MethodSignature) joinPoint.getSignature());
46
47 HttpServletRequest httpRequest = getHttpServletRequest();
48 String signatureType = httpRequest.getHeader("x-signature-type");
49 String signatureData = httpRequest.getHeader("x-signature-data");
50 String requestBody = getRequestBody(httpRequest);
51
52 // 校验
53 }