wayne
wayne
发布于 2026-02-24 / 0 阅读
0
0

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


评论