在 Spring Boot Filter 中获取响应体

1、简介

本文将带你了解如何在 Spring Boot Filter(过滤器)中获取 ServletResponse 的响应体。

2、场景

在使用 Spring Boot 中使用 Filter 时,从 ServletResponse 访问响应体非常麻烦。这是因为响应体不是随时可用的,它是在 Filter 链执行完毕后才写入输出流的。

但是,有些操作(如生成哈希签名)需要在发送给客户端之前读取完整的响应正文的内容。因此,需要找到读取响应体内容的方法。

3、使用 ContentCachingResponseWrapper

创建一个自定义 Filter,并使用 Spring 提供的 ContentCachingResponseWrapper 类进行包装:

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) 
  throws IOException, ServletException {
    ContentCachingResponseWrapper responseCacheWrapperObject = 
      new ContentCachingResponseWrapper((HttpServletResponse) servletResponse);
    filterChain.doFilter(servletRequest, responseCacheWrapperObject);
    byte[] responseBody = responseCacheWrapperObject.getContentAsByteArray();
    MessageDigest md5Digest = MessageDigest.getInstance("MD5");
    byte[] md5Hash = md5Digest.digest(responseBody);
    String md5HashString = DatatypeConverter.printHexBinary(md5Hash);
    responseCacheWrapperObject.getResponse().setHeader("Response-Body-MD5", md5HashString);
    // ...
}

简而言之,wrapper 类允许我们封装 HttpServletResponse 以缓存响应正文内容,并调用 doFilter() 将请求传递给下一个 Filter。

注意,不能忘记在这里调用 doFilter()。否则,传入的请求将不会进入 Spring Boot Filter 链中的下一个 Filter,应用不会按照预期处理请求。事实上,不调用 doFilter() 违反了 servlet 规范

此外,一定不能忘记是使用 responseCacheWrapperObject 对象作为参数来调用 doFilter()。否则,响应体将不会被缓存。简而言之,ContentCachingResponseWrapper 将 Filter 放在了响应输出流和发出 HTTP 请求的客户端之间。因此,在创建响应正文输出流时,也就是在本例中调用 doFilter() 之后,就可以在过滤器中处理响应正文的内容了。

使用 wrapper 后,可通过 getContentAsByteArray() 方法从 Filter 中获取响应正文,并计算 MD5 哈希值。

首先,使用 MessageDigest 类创建响应正文的 MD5 哈希值。其次,将字节数组转换为十六进制字符串。最后,使用 setHeader() 方法将生成的哈希字符串设置为 response 对象的 Header。

最后,在退出 doFilter() 方法之前调用 copyBodyToResponse() 以将更新后的响应正文复制到原始响应中:

responseCacheWrapperObject.copyBodyToResponse();

这一步至关重要,否则,客户端无法收到完整的响应。

4、配置 Filter

现在,在 Spring Boot 中添加 Filter:

@Bean
public FilterRegistrationBean loggingFilter() {
    FilterRegistrationBean registrationBean = new FilterRegistrationBean<>();
    registrationBean.setFilter(new MD5Filter());
    return registrationBean;
}

如上,配置创建一个 FilterRegistrationBean,其中包含之前创建的 Filter 的实现。

5、测试 MD5

使用 Spring 中的集成测试来进行测试:

@Test
void whenExampleApiCallThenResponseHasMd5Header() throws Exception {
    String endpoint = "/api/example";
    String expectedResponse = "Hello, World!";
    String expectedMD5 = getMD5Hash(expectedResponse);

    MvcResult mvcResult = mockMvc.perform(get(endpoint).accept(MediaType.TEXT_PLAIN_VALUE))
      .andExpect(status().isOk())
      .andReturn();

    String md5Header = mvcResult.getResponse()
      .getHeader("Response-Body-MD5");
    assertThat(md5Header).isEqualTo(expectedMD5);
}

如上,调用了 /api/example Controller,它在正文中返回了 “Hello, World!“文本。

定义的 getMD5Hash() 方法,该方法可将响应转换为 MD5,类似于在 Filter 中使用的 MD5:

private String getMD5Hash(String input) throws NoSuchAlgorithmException {
    MessageDigest md5Digest = MessageDigest.getInstance("MD5");
    byte[] md5Hash = md5Digest.digest(input.getBytes(StandardCharsets.UTF_8));
    return DatatypeConverter.printHexBinary(md5Hash);
}

6、总结

本文介绍了如何使用 ContentCachingResponseWrapper 类在 Spring Boot Filter 获取、修改 ServletResponse 的响应体,以及如何计算响应体的 MD5 哈希值。


Ref:https://www.baeldung.com/spring-boot-filter-response-body