目录

HttpServletRequest 相关

解决 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;
    }
}

继承 HttpServletRequestWrapper 缓存 byte[] 重写 ServletInputStream

所幸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("/**");
  }
}