从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. 注意处理字符编码,避免乱码。

相关推荐
Mahir082 小时前
Spring 循环依赖深度解密:从问题本质到三级缓存源码级解析
java·后端·spring·缓存·面试·循环依赖·三级缓存
RyFit3 小时前
SpringAI 常见问题及解决方案大全
java·ai
石山代码3 小时前
C++ 内存分区 堆区
java·开发语言·c++
绝知此事3 小时前
【算法突围 01】线性结构与哈希表:后端开发的收纳术
java·数据结构·算法·面试·jdk·散列表
无风听海3 小时前
C# 隐式转换深度解析
java·开发语言·c#
一只大袋鼠4 小时前
Git 进阶(二):分支管理、暂存栈、远程仓库与多人协作
java·开发语言·git
德思特5 小时前
从 Dify 配置页理解 RAG 的重要参数
java·人工智能·llm·dify·rag
YOU OU5 小时前
Spring IoC&DI
java·数据库·spring
один but you5 小时前
从可变参数到 emplace:现代 C++ 性能优化的核心组合
java·开发语言
IT_陈寒5 小时前
Redis缓存击穿把我整不会了,原来还有这手操作
前端·人工智能·后端