You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义OncePerRequestFilter读取空请求体:请求体为何已被消费?

问题背景

开发集成Spring Security的Spring Boot应用时,需要为/api/v1/mpos/set-token端点的POST请求补全缺失的Content-Type(默认application/json)和Content-Length头部。为此编写了继承OncePerRequestFilter的自定义过滤器,通过包装HttpServletRequest缓存请求体,但调用input.read(buffer)时立即返回-1,缓存的请求体为空数组。已验证过滤器优先级为最高,且执行顺序早于其他过滤器。

问题1:什么会导致请求体在过滤器读取前被消费?

  • Spring Security过滤器链优先级冲突:尽管你设置了@Order(Ordered.HIGHEST_PRECEDENCE),但Spring Security的FilterChainProxy默认Order为-100,若你的过滤器通过@Component自动注册,可能因Spring自动配置的优先级逻辑,导致FilterChainProxy实际先执行(它会提前读取请求体用于CSRF校验或其他安全逻辑)。
  • 请求本身无请求体:客户端发送的POST请求为空体,此时输入流自然返回EOF(-1),属于正常情况。
  • Servlet容器预读取:部分Servlet容器(如Tomcat)会在处理请求时提前读取请求体做编码转换或其他预处理,导致流被提前消费。
  • 隐式第三方过滤器:某些监控、日志类第三方组件的过滤器(如请求日志过滤器)可能被自动注册且优先级更高,提前读取了请求体。

问题2:如何确保过滤器获取完整请求体以添加默认头部?

  1. 强制过滤器优先级最高:放弃@Component+@Order的方式,改用FilterRegistrationBean手动注册过滤器,明确设置Order为Integer.MIN_VALUE,确保绝对优先执行:
    @Configuration
    public class FilterConfig {
        @Bean
        public FilterRegistrationBean<SetTokenDefaultHeadersFilter> setTokenFilterRegistration() {
            FilterRegistrationBean<SetTokenDefaultHeadersFilter> registrationBean = new FilterRegistrationBean<>();
            registrationBean.setFilter(new SetTokenDefaultHeadersFilter());
            registrationBean.setOrder(Integer.MIN_VALUE);
            registrationBean.addUrlPatterns("/api/v1/mpos/set-token");
            return registrationBean;
        }
    }
    
  2. 修正请求URI判断逻辑:用request.getServletPath()代替request.getRequestURI(),避免上下文路径干扰(如应用部署在/app下时,getRequestURI会包含/app前缀):
    if (TARGET_URI.equals(request.getServletPath()) && "POST".equalsIgnoreCase(request.getMethod())) {
        // 逻辑处理
    }
    
  3. 延迟缓存请求体:将请求体缓存逻辑从HttpServletRequestWrapper的初始化阶段,延迟到第一次调用getInputStream()或getReader()时执行,避免提前读取导致的流消费问题:
    private byte[] cachedBody;
    private byte[] getCachedBody() throws IOException {
        if (cachedBody == null) {
            cachedBody = toByteArray(request.getInputStream());
        }
        return cachedBody;
    }
    // 重写getInputStream时调用getCachedBody()
    
  4. 处理空请求体场景:读取流前检查request.getContentLengthLong()是否为0,若为空则直接设置Content-Length: 0,避免无意义的流读取。

问题3:Spring Boot推荐的预处理前缓存请求体的方法?

Spring官方推荐使用ContentCachingRequestWrapper,这是Spring内置的请求包装类,专门用于缓存请求体,无需手动实现流读取和缓存逻辑:

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
    if (TARGET_URI.equals(request.getServletPath()) && "POST".equalsIgnoreCase(request.getMethod())) {
        ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
        
        // 补全默认头部(若缺失)
        if (wrappedRequest.getHeader("Content-Type") == null) {
            wrappedRequest.addHeader("Content-Type", DEFAULT_CONTENT_TYPE);
        }
        
        // 触发缓存并传递给后续链
        filterChain.doFilter(wrappedRequest, response);
        
        // 补全Content-Length(若缺失)
        if (wrappedRequest.getHeader("Content-Length") == null) {
            response.setContentLength(wrappedRequest.getContentAsByteArray().length);
        }
    } else {
        filterChain.doFilter(request, response);
    }
}

ContentCachingRequestWrapper会在第一次读取请求体时自动缓存,后续可通过getContentAsByteArray()获取完整请求体,无需手动处理流的读取和包装逻辑。

内容的提问来源于stack exchange,提问作者Roman Patrushev

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 15:07:08