Spring Boot部署在Heroku时,如何为JSESSIONID Cookie设置SameSite=None以解决Chrome跨域Cookie拦截问题
我来帮你搞定Chrome里JSESSIONID被拦截的问题!这个情况是因为Chrome现在对跨站Cookie的SameSite属性有严格要求,必须同时设置SameSite=None和Secure才能在跨站场景(比如你的Angular应用调用Heroku上的API)下正常工作,而Spring Boot默认的JSESSIONID并没有配置这些属性。
下面给你几种可行的解决方案,按推荐程度排序:
方法一:通过Spring Boot配置文件直接设置(最简单)
如果你的Spring Boot版本是2.6及以上,可以直接在application.properties或者application.yml里添加配置,全局修改JSESSIONID的Cookie属性:
application.properties:
# 设置JSESSIONID的SameSite属性为None server.servlet.session.cookie.same-site=None # 必须开启Secure,因为SameSite=None要求Cookie只能通过HTTPS传输(Heroku是HTTPS环境,没问题) server.servlet.session.cookie.secure=true # 可选:如果需要Cookie在整个站点可用,设置路径为/ server.servlet.session.cookie.path=/
application.yml:
server: servlet: session: cookie: same-site: None secure: true path: /
这种方式不需要修改代码,直接配置就行,非常方便。
方法二:通过Spring Security配置JSESSIONID Cookie
如果你的项目用了Spring Security,也可以在SecurityConfig的configure(HttpSecurity http)方法里直接配置session的Cookie属性,这样更贴合安全配置逻辑:
修改你的SecurityConfig类:
@Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers(HttpMethod.POST, PUBLIC_MATCHERS_POST).permitAll() .antMatchers(HttpMethod.GET, PUBLIC_MATCHERS_GET).permitAll() .antMatchers(PUBLIC_MATCHERS).permitAll() .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() .anyRequest().authenticated() .and().formLogin() .and().httpBasic() .and().logout().logoutUrl("/logout").logoutSuccessHandler((new HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK))) .clearAuthentication(true).invalidateHttpSession(true) .deleteCookies("JSESSIONID", "XSRF-TOKEN") // 新增JSESSIONID Cookie配置 .and().sessionManagement() .sessionCookieConfig() .sameSite(SameSiteCookie.NONE) // 设置SameSite为None .secure(true) // 开启Secure属性 .path("/"); // 设置Cookie路径为整个站点 }
注意:这个方式需要Spring Security版本在5.6及以上,因为SameSiteCookie枚举是在这个版本引入的。
方法三:自定义Filter修改JSESSIONID Cookie属性(兜底方案)
如果上面两种方法因为版本限制无法使用,可以自定义一个Filter来修改响应中的JSESSIONID Cookie头。你可以修改现有的CorsFilter,或者新增一个Filter:
修改后的CorsFilter代码:
@Component @Order(Ordered.HIGHEST_PRECEDENCE) public class CorsFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; // 先设置CORS相关头(保持你原来的配置) response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin")); response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE, PATCH"); response.setHeader("Access-Control-Max-Age", "3600"); response.setHeader("Access-Control-Allow-Headers", "X-PINGOTHER, Content-Type, X-Requested-With, Accept, Origin, Access-Control-Request-Method, " + "Access-Control-Request-Headers, Authorization, if-modified-since, remember-me, " + "x-csrf-token, x-xsrf-token, xsrf-token "); response.addHeader("Access-Control-Expose-Headers", "xsrf-token"); response.addHeader("Access-Control-Allow-Headers", "x-csrf-token, x-xsrf-token"); // 用包装类捕获Spring生成的Set-Cookie头 CustomHttpServletResponseWrapper wrappedResponse = new CustomHttpServletResponseWrapper(response); chain.doFilter(request, wrappedResponse); // 处理所有Cookie,为JSESSIONID添加SameSite=None和Secure for (String cookie : wrappedResponse.getSetCookieHeaders()) { String updatedCookie; if (cookie.contains("JSESSIONID")) { // 确保添加SameSite=None和Secure,避免重复添加 if (!cookie.contains("SameSite=None")) { updatedCookie = cookie + "; SameSite=None; Secure"; } else { updatedCookie = cookie; } } else { updatedCookie = cookie; } response.addHeader("Set-Cookie", updatedCookie); } // 你的locale Cookie保持不变 response.addHeader("Set-Cookie", "locale=pt-BR; HttpOnly; Secure; SameSite=None;"); } @Override public void init(FilterConfig filterConfig) throws ServletException {} @Override public void destroy() {} // 自定义HttpServletResponse包装类,用于捕获Set-Cookie头 private static class CustomHttpServletResponseWrapper extends HttpServletResponseWrapper { private final List<String> setCookieHeaders = new ArrayList<>(); public CustomHttpServletResponseWrapper(HttpServletResponse response) { super(response); } @Override public void addHeader(String name, String value) { if ("Set-Cookie".equalsIgnoreCase(name)) { setCookieHeaders.add(value); } else { super.addHeader(name, value); } } @Override public void setHeader(String name, String value) { if ("Set-Cookie".equalsIgnoreCase(name)) { setCookieHeaders.clear(); setCookieHeaders.add(value); } else { super.setHeader(name, value); } } public List<String> getSetCookieHeaders() { return setCookieHeaders; } } }
关键注意点
- 必须同时设置
SameSite=None和Secure:Chrome明确要求,跨站Cookie设置SameSite=None时必须开启Secure,否则会被拦截。Heroku是HTTPS环境,所以这个要求完全满足。 - CORS配置要正确:确保你的
allowedOrigins是具体的域名(不能用*),并且allowCredentials(true)已经开启,你现有配置里已经做了这点,没问题。 - 测试验证:修改后部署到Heroku,打开Chrome开发者工具(F12),进入
Application->Cookies,查看你的API域名下的JSESSIONID,确认SameSite属性为None,Secure为true。
内容的提问来源于stack exchange,提问作者Andre
相关产品推荐
相关产品推荐

