从Spring拦截器到Filter过滤器:一次报文修改加解密的填坑经验

从Spring拦截器到Filter过滤器:一次报文修改加解密的填坑经验

​欢迎光临小站:致橡树

最近在项目中遇到一个需求:对某些敏感接口的请求和响应报文进行 AES 加密解密。本以为用 Spring 的拦截器就能轻松搞定,结果踩了坑------拦截器根本没法修改响应体。最终通过 Servlet Filter 完美解决。本文将结合这次经历,深入对比拦截器(Interceptor)和过滤器(Filter)的区别,以及它们各自的适用场景。

需求背景

我们的 CRM 系统中,有几个核心接口(如客户注册、查询、更新)需要保证数据传输安全。前端会发送如下格式的请求:

复制代码
{
  "data": "加密后的字符串"
}

后端需要先解密 data 字段,然后执行业务逻辑;返回时也要将响应数据加密成同样的格式。整个过程对业务代码完全透明。

第一次尝试:使用 Spring 拦截器

我最初的想法很直接:在 preHandle 中解密请求体,在 postHandle 中加密响应体。但很快遇到了问题:

  1. 请求体读取HttpServletRequest 的输入流只能读一次,在拦截器中读过后,后面的 Controller 就无法读取了。这个问题可以通过自定义 RequestWrapper 解决。

  2. 响应体修改postHandle 虽然能拿到 ModelAndView,但我们的接口返回的是 JSON 数据(通过 @ResponseBodyResponseEntity),此时响应已经写入 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 可以直接操作 ServletRequestServletResponse,因此可以通过自定义 RequestWrapperResponseWrapper 对请求体和响应体进行完全控制(包括替换、修改内容)。

  • Interceptor 只能访问 HttpServletRequestHttpServletResponse 以及处理完的 ModelAndView。对于响应体,一旦 Controller 返回并写入 OutputStream,Interceptor 就无法干预了。

3. 应用场景

场景 Filter Interceptor
修改请求/响应内容 ✅ 可以通过包装器实现(如加解密、压缩、解压) ❌ 无法修改响应体,请求体也只能通过包装器配合
权限/日志 ✅ 可以,但通常权限校验放在 Interceptor 更语义化 ✅ 适合做登录校验、日志记录、性能监控
与业务解耦的通用处理 ✅ 字符编码、跨域头、XSS 过滤等 ✅ 权限检查、多语言切换等
异常处理 只能捕获 Filter 内部异常,无法处理 Controller 抛出的业务异常 可以通过 @ExceptionHandlerafterCompletion 处理

简单来说,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 的用法,有几点值得注意:

  1. 需要手动将 Filter 注册为 Spring Bean,并设置 @Order 确保它在最前面执行。

  2. 包装响应时,一定要确保在 chain.doFilter 之后再读取捕获的内容并重新输出,否则响应已经提交。

  3. 注意处理字符编码,避免乱码。

相关推荐
J2虾虾1 小时前
Spring Boot中使用@Scheduled做定时任务
java·前端·spring boot
snakeshe10102 小时前
Java集合框架深度解析:核心类库与实战应用
后端
肉肉不想干后端2 小时前
联合订单并发退款:一次分布式锁冲突的排查与思考
java
大鹏19882 小时前
告别 XML 与字符串拼接:dbVisitor 如何以“多范式融合”重塑 Java DAL 层
后端
你有医保你先上2 小时前
go-es:一个优雅的 Elasticsearch Go 客户端
后端·elasticsearch
用户4745189475102 小时前
全链路日志追踪利器:trace-spring-boot-starter 实战指南
java
acx匿2 小时前
【Windows10 下 JDK17 环境变量配置超详细教程(ZIP 版)】
java·jdk
Renhao-Wan2 小时前
Java 算法实践(七):动态规划
java·算法·动态规划
新缸中之脑2 小时前
Sonnet 4.6 vs Opus 4.6
java·开发语言