Spring Boot过滤器读取Cookie header随机失效,添加日志则恢复正常
问题描述
- 后端自定义
CookieValidationFilter过滤器,校验请求Cookie头中是否存在SessionCookie - 前端Angular向安全端点发送POST请求时,偶发后端无法获取Cookie,触发
401 Unauthorized、CORS错误,浏览器控制台出现no-referrer警告 - 只要在过滤器中添加打印请求头的调试日志,问题立即消失,请求恢复正常
- 前端已正确设置
SessionCookie(浏览器DevTools的Cookies标签可见),但请求失败时Network标签中无Cookie头,添加日志后则正常发送 - Cookie配置为
HttpOnly; Secure; SameSite=None
相关代码
ContentCachingRequestFilter
@Component public class ContentCachingRequestFilter implements Filter { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest) servletRequest); filterChain.doFilter(requestWrapper, servletResponse); } }
CookieValidationFilter(简化版)
@Component public class CookieValidationFilter extends OncePerRequestFilter { private final ObjectMapper objectMapper; public CookieValidationFilter(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String cookieHeader = request.getHeader(HttpHeaders.COOKIE); boolean hasSessionCookie = cookieHeader != null && Arrays.stream(cookieHeader.split(";")) .map(String::trim) .anyMatch(c -> c.startsWith("SessionCookie=") && !c.equalsIgnoreCase("SessionCookie=")); if (!hasSessionCookie) { ApiResponse apiResponse = ApiResponse.builder().httpStatus(Constants.UNAUTHORISED_ACCESS_CODE) .statusMessage(Constants.UNAUTHORIZED_MSG) .userMessage(Constants.YOU_ARE_NOT_AUTHORIZED_TO_ACCESS_THE_FEATURE_MSG).errorMessage(null) .data(null).messageSeverity(null).validationErrors(null).build(); response.setStatus(Constants.UNAUTHORISED_ACCESS_CODE); response.setContentType("application/json"); response.getWriter().write(objectMapper.writeValueAsString(apiResponse)); return; } filterChain.doFilter(request, response); } }
已尝试方案
- 调用
Collections.list(request.getHeaderNames()).forEach(request::getHeader)强制访问所有请求头 - 将
ContentCachingRequestFilter改为继承OncePerRequestFilter - 确保CORS配置正确(开启
allowCredentials(true)、设置精确请求源) - 前端请求配置
withCredentials: true - Cookie已设置
SameSite=None; Secure; HttpOnly
问题原因分析
这个诡异现象的核心原因是请求头延迟解析+ContentCachingRequestWrapper的特性:
- Tomcat默认采用请求头延迟解析机制,只有首次调用
getHeader()、getHeaderNames()等方法时,才会从请求流中解析头信息 ContentCachingRequestWrapper仅缓存请求体,若请求头未提前解析,直接传递包装后的请求到后续过滤器,会导致头解析时机异常- 添加日志打印请求头时,会提前触发Tomcat的头解析逻辑,确保后续过滤器能正常获取Cookie头;无日志时,头解析可能被延迟到后续步骤,导致
CookieValidationFilter执行时头未被解析,获取到null的Cookie头
此外,过滤器执行顺序若未明确,可能导致ContentCachingRequestFilter在CookieValidationFilter之后执行,进一步加剧头解析时机问题。
解决方案
1. 修正ContentCachingRequestFilter,强制提前解析请求头
修改内容缓存过滤器,在包装请求后立即触发头解析,避免后续过滤器无法获取头信息:
@Component @Order(1) // 确保优先执行 public class ContentCachingRequestFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 强制解析所有请求头,避免延迟解析导致的问题 Collections.list(request.getHeaderNames()).forEach(request::getHeader); ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request); filterChain.doFilter(requestWrapper, response); } }
2. 优化Cookie校验逻辑,使用Servlet原生API
直接使用request.getCookies()替代手动解析Cookie头,该方法会自动触发头解析,同时避免字符串解析的潜在错误:
@Component @Order(2) // 在内容缓存过滤器之后执行 public class CookieValidationFilter extends OncePerRequestFilter { private final ObjectMapper objectMapper; public CookieValidationFilter(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 处理getCookies返回null的情况,避免空指针 Cookie[] cookies = request.getCookies(); boolean hasSessionCookie = cookies != null && Arrays.stream(cookies) .anyMatch(cookie -> "SessionCookie".equals(cookie.getName()) && cookie.getValue() != null && !cookie.getValue().isEmpty()); if (!hasSessionCookie) { ApiResponse apiResponse = ApiResponse.builder().httpStatus(Constants.UNAUTHORISED_ACCESS_CODE) .statusMessage(Constants.UNAUTHORIZED_MSG) .userMessage(Constants.YOU_ARE_NOT_AUTHORIZED_TO_ACCESS_THE_FEATURE_MSG).errorMessage(null) .data(null).messageSeverity(null).validationErrors(null).build(); response.setStatus(Constants.UNAUTHORISED_ACCESS_CODE); response.setContentType("application/json"); response.getWriter().write(objectMapper.writeValueAsString(apiResponse)); return; } filterChain.doFilter(request, response); } }
3. 明确过滤器执行顺序
通过@Order注解指定过滤器优先级,确保ContentCachingRequestFilter在CookieValidationFilter之前执行,保证头解析提前完成。
内容的提问来源于stack exchange,提问作者Shailesh Sorathiya
相关产品推荐
相关产品推荐

