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

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

​欢迎光临小站:致橡树

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

需求背景

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

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

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

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

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

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

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

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

相关推荐
2301_780789665 小时前
跨平台适配!SDK 游戏盾兼容全终端游戏场景
java·网络·ddos
学长毕设6 小时前
基于springboot的食品仓库管理系统
java·spring boot·后端
滴滴答答哒7 小时前
ASP.NET Core + FaceRecognitionDotNet 人脸识别实战:DI 单例、特征向量持久化与 1:N 比对
后端·asp.net
步行cgn7 小时前
FormHttpMessageConverter 详解:Spring MVC 中表单数据的读写转换器
java·spring·mvc
龙亘川7 小时前
构建城市公共安全数字底座:亘川智城智慧安防系统全维度解读
java·大数据·开发语言·人工智能·信息可视化
金銀銅鐵7 小时前
[Java] 用 CLI 展示 EnumMap 增删键值对时的内部变化
java·源码·ai编程
秋天的怀念_David7 小时前
第二十二夜 · 数字迷雾
java·程序人生·算法
Raas1008 小时前
AI网关部署方案?从部署到上线全流程
java·开发语言·人工智能·gateway·ai网关·企业级ai网关·企业级ai产品
niucloud-admin8 小时前
NIUCLOUD PHP SAAS上门家政使用文档:城市管理
java·前端·php
霸道流氓气质9 小时前
智能客服组件:分层意图识别、RAG Advisor 与人工转接状态机的 Vue3 + Spring AI 生产级实战
java·人工智能·spring