Spring Boot Filter中截取响应输出内容

在本文中,我们将探讨如何从Spring Boot 过滤器中的ServletResponse检索响应正文。

本质上,我们将定义问题,然后使用缓存响应正文的解决方案,使其在 Spring Boot 过滤器中可用。让我们开始。

首先,让我们了解我们要解决的问题。

使用 Spring Boot 过滤器时,从 ServletResponse 访问响应主体很 棘手。这是因为响应主体不容易获得,因为它是在过滤器链完成执行后写入输出流的。

但是,某些操作(例如生成哈希签名)需要响应正文的内容,然后才能将其发送到客户端。因此,我们需要找到一种方法来读取body的内容。

在过滤器中使用ContentCachingResponseWrapper
为了克服前面定义的问题,我们将创建一个自定义过滤器并使用Spring Framework 提供的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);
   
// ...
}

简而言之,包装类允许我们包装HttpServletResponse来缓存响应正文内容,并调用doFilter()将请求传递到下一个过滤器。

请记住,我们一定不要忘记这里的doFilter()调用。否则,传入的请求将不会进入 Spring 过滤器链中的下一个过滤器,应用程序也不会按照我们的预期处理该请求。事实上,不调用doFilter()是违反servlet规范的。

此外,我们一定不要忘记使用responseCacheWrapperObject调用doFilter ()。否则,响应正文将不会被缓存。简而言之,ContentCachingResponseWrapper将过滤器放置在响应输出流和发出 HTTP 请求的客户端之间。因此,在创建响应主体输出流后(在本例中是在doFilter()调用之后),内容可在过滤器内进行处理。

使用包装器后,可以使用getContentAsByteArray()方法在过滤器中获取响应正文。我们使用这个方法来计算MD5哈希值。

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

如果需要,我们可以将字节数组转换为字符串,并使正文的内容更加明确。

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

responseCacheWrapperObject.copyBodyToResponse();

在退出doFilter()方法之前调用copyBodyToResponse()至关重要。否则,客户端将不会收到完整的响应。

配置过滤器
现在,我们需要在 Spring Boot 中添加过滤器:

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

在这里,我们使用 我们之前创建的过滤器的实现来配置创建一个FilterRegistrationBean 。

测试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控制器,它返回“Hello, World!”正文中的文字。我们定义了getMD5Hash()方法,它将响应转换为 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);
}

结论
在本文中,我们学习了如何使用ContentCachingResponseWrapper类从Spring Boot 过滤器中的ServletResponse检索响应正文。我们使用此机制来展示如何在 HTTP 响应标头中实现正文的 MD5 编码。