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

Spring Boot过滤器读取Cookie header随机失效,添加日志则恢复正常

Spring Boot + Angular 中Cookie过滤器的诡异问题及解决方案

问题描述

  • 后端自定义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的特性:

  1. Tomcat默认采用请求头延迟解析机制,只有首次调用getHeader()、getHeaderNames()等方法时,才会从请求流中解析头信息
  2. ContentCachingRequestWrapper仅缓存请求体,若请求头未提前解析,直接传递包装后的请求到后续过滤器,会导致头解析时机异常
  3. 添加日志打印请求头时,会提前触发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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.12 19:07:34