从Spring拦截器到Filter过滤器:一次报文修改加解密的填坑经验
最近在项目中遇到一个需求:对某些敏感接口的请求和响应报文进行 AES 加密解密。本以为用 Spring 的拦截器就能轻松搞定,结果踩了坑——拦截器根本没法修改响应体。最终通过 Servlet Filter 完美解决。本文将结合这次经历,深入对比拦截器(Interceptor)和过滤器(Filter)的区别,以及它们各自的适用场景。
需求背景
我们的 CRM 系统中,有几个核心接口(如客户注册、查询、更新)需要保证数据传输安全。前端会发送如下格式的请求:
{
"data": "加密后的字符串"
}后端需要先解密 data 字段,然后执行业务逻辑;返回时也要将响应数据加密成同样的格式。整个过程对业务代码完全透明。
第一次尝试:使用 Spring 拦截器
我最初的想法很直接:在 preHandle 中解密请求体,在 postHandle 中加密响应体。但很快遇到了问题:
请求体读取:
HttpServletRequest的输入流只能读一次,在拦截器中读过后,后面的 Controller 就无法读取了。这个问题可以通过自定义RequestWrapper解决。响应体修改:
postHandle虽然能拿到ModelAndView,但我们的接口返回的是 JSON 数据(通过@ResponseBody或ResponseEntity),此时响应已经写入OutputStream,无法再修改。
即使我在 postHandle 中尝试修改响应头或重新写入,也会因为响应已提交而失败。拦截器根本不适合做响应体的篡改。
过滤器 vs 拦截器:深入对比
既然拦截器行不通,自然想到了 Servlet 规范中的 Filter。先来梳理一下两者的核心区别。
1. 所处层级不同
Filter 是 Servlet 规范的一部分,由 Servlet 容器(如 Tomcat)管理。它基于请求的回调机制,在请求进入 Servlet 前和后进行拦截。
Interceptor 是 Spring MVC 提供的特性,基于 Spring 的 AOP 机制,作用于 Spring 管理的 Controller 前后。
执行顺序大致如下:
请求 → Filter 前置逻辑 → DispatcherServlet → Interceptor preHandle → Controller → Interceptor postHandle → Interceptor afterCompletion → Filter 后置逻辑 → 响应2. 能操作的对象不同
Filter 可以直接操作
ServletRequest和ServletResponse,因此可以通过自定义RequestWrapper和ResponseWrapper对请求体和响应体进行完全控制(包括替换、修改内容)。Interceptor 只能访问
HttpServletRequest、HttpServletResponse以及处理完的ModelAndView。对于响应体,一旦 Controller 返回并写入OutputStream,Interceptor 就无法干预了。
3. 应用场景
简单来说,Filter 更底层,适合对请求/响应报文做“物理级”的修改;Interceptor 更贴近业务,适合做“逻辑级”的拦截处理。
最终方案:用 Filter 实现报文加解密
下面展示如何用 Filter + 自定义 Request/ResponseWrapper 实现透明加解密。
1. 自定义请求包装器:EncryptRequestWrapper
public class EncryptRequestWrapper extends HttpServletRequestWrapper {
private byte[] body;
public EncryptRequestWrapper(HttpServletRequest request) {
super(request);
// 读取原始请求体
body = ServletUtils.getBodyBytes(request);
}
@Override
public ServletInputStream getInputStream() {
ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() { return bais.read(); }
@Override public boolean isFinished() { return false; }
@Override public boolean isReady() { return false; }
@Override public void setReadListener(ReadListener listener) {}
};
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
public void updateBody(byte[] newBody) { this.body = newBody; }
public void updateBody(String newBody) { this.body = newBody.getBytes(StandardCharsets.UTF_8); }
public byte[] getBody() { return body; }
}2. 自定义响应包装器:EncryptResponseWrapper
public class EncryptResponseWrapper extends HttpServletResponseWrapper {
private final ByteArrayOutputStream capture;
private ServletOutputStream output;
private PrintWriter writer;
public EncryptResponseWrapper(HttpServletResponse response) {
super(response);
capture = new ByteArrayOutputStream(response.getBufferSize());
}
@Override
public ServletOutputStream getOutputStream() {
if (writer != null) throw new IllegalStateException("...");
if (output == null) {
output = new ServletOutputStream() {
@Override public void write(int b) { capture.write(b); }
@Override public boolean isReady() { return false; }
@Override public void setWriteListener(WriteListener listener) {}
};
}
return output;
}
@Override
public PrintWriter getWriter() throws IOException {
if (output != null) throw new IllegalStateException("...");
if (writer == null) {
writer = new PrintWriter(new OutputStreamWriter(capture, getCharacterEncoding()));
}
return writer;
}
public byte[] getResponseData() throws IOException {
if (writer != null) writer.close();
else if (output != null) output.close();
return capture.toByteArray();
}
}3. 核心过滤器:DecryptRequestFilter
@Component
@Order(HIGHEST_PRECEDENCE)
public class DecryptRequestFilter implements Filter {
@Resource private CrmSetting crmSetting;
private final ObjectMapper objectMapper = new ObjectMapper();
private static final Set<String> ENCRYPTED_PATHS = Set.of("/customer/register/v1", ...);
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String path = httpRequest.getRequestURI();
if (shouldProcess(httpRequest, path)) {
// 1. 包装请求并解密
EncryptRequestWrapper wrappedRequest = new EncryptRequestWrapper(httpRequest);
try {
String decrypted = attemptDecrypt(wrappedRequest);
if (decrypted != null) wrappedRequest.updateBody(decrypted);
} catch (BusinessException e) {
// 返回解密失败的错误信息
writeErrorResponse((HttpServletResponse) response, e);
return;
}
// 2. 包装响应
EncryptResponseWrapper wrappedResponse = new EncryptResponseWrapper((HttpServletResponse) response);
// 3. 继续过滤器链
chain.doFilter(wrappedRequest, wrappedResponse);
// 4. 响应加密
encryptResponse(wrappedResponse, response);
} else {
chain.doFilter(request, response);
}
}
private void encryptResponse(EncryptResponseWrapper wrappedResponse, ServletResponse originalResponse) {
byte[] responseData = wrappedResponse.getResponseData();
String responseStr = new String(responseData, StandardCharsets.UTF_8);
ResultMsg<?> resultMsg = objectMapper.readValue(responseStr, ResultMsg.class);
if (resultMsg != null && resultMsg.getData() != null) {
String encryptedData = encrypt(resultMsg.getData());
resultMsg.setData(encryptedData);
originalResponse.getWriter().write(objectMapper.writeValueAsString(resultMsg));
} else {
originalResponse.getWriter().write(responseStr);
}
}
private String attemptDecrypt(EncryptRequestWrapper request) {
// 读取 body,判断是否 { "data": "加密串" },解密并返回明文 JSON
}
private boolean shouldProcess(HttpServletRequest request, String path) {
return ENCRYPTED_PATHS.stream().anyMatch(path::contains) &&
crmSetting.getAesSecretChannel().contains(request.getHeader("channel"));
}
}4. 关键点解析
请求体替换:通过
EncryptRequestWrapper缓存原始 body,解密后调用updateBody替换为明文 JSON,后续 Controller 就能正常读取。响应体捕获:
EncryptResponseWrapper将输出内容写入内部的ByteArrayOutputStream,等业务处理完后,我们从包装器中获取完整响应,加密后再通过原始HttpServletResponse输出。
总结
通过这次实践,我深刻体会到选对工具的重要性:
如果需要对请求/响应报文进行“手术级”的修改(如加密、压缩、替换内容),必须使用 Filter,因为它能通过包装器完全控制输入输出流。
如果只是需要在 Controller 前后做一些业务无关的横切逻辑(如权限校验、日志记录),Interceptor 更简洁,且能与 Spring 生态无缝集成(如注入 Bean、访问
ModelAndView)。
另外,对于 Filter 的用法,有几点值得注意:
需要手动将 Filter 注册为 Spring Bean,并设置
@Order确保它在最前面执行。包装响应时,一定要确保在
chain.doFilter之后再读取捕获的内容并重新输出,否则响应已经提交。注意处理字符编码,避免乱码。