自定义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:如何确保过滤器获取完整请求体以添加默认头部?
- 强制过滤器优先级最高:放弃
@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; } } - 修正请求URI判断逻辑:用
request.getServletPath()代替request.getRequestURI(),避免上下文路径干扰(如应用部署在/app下时,getRequestURI会包含/app前缀):if (TARGET_URI.equals(request.getServletPath()) && "POST".equalsIgnoreCase(request.getMethod())) { // 逻辑处理 } - 延迟缓存请求体:将请求体缓存逻辑从
HttpServletRequestWrapper的初始化阶段,延迟到第一次调用getInputStream()或getReader()时执行,避免提前读取导致的流消费问题:private byte[] cachedBody; private byte[] getCachedBody() throws IOException { if (cachedBody == null) { cachedBody = toByteArray(request.getInputStream()); } return cachedBody; } // 重写getInputStream时调用getCachedBody() - 处理空请求体场景:读取流前检查
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
相关产品推荐
相关产品推荐

