在 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