如何在 JSESSIONID Cookie 中设置 SameSite=None
How to set SameSite=None in JSESSIONID Cookie
我有一个 spring boot API 托管在 Heroku 上,当我尝试通过 Google Chrome 中的 Angular 应用访问它时(在Firefox 它工作正常)我面临以下问题:
似乎 JSESSIONID cookie 被阻止了,因为它没有设置为 SameSite=None。但是如何将其设置为 SameSite=None?
以下是我的配置类:
安全配置:
@Configuration
@EnableWebSecurity
@Order(SecurityProperties.DEFAULT_FILTER_ORDER)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private ClienteUserDetailsService clienteUserDetailsService;
private static final String[] PUBLIC_MATCHERS = {"/login", "/logout", "/error.html", "/error"};
private static final String[] PUBLIC_MATCHERS_GET = {"/login", "/logout", "/error.html", "/error"};
private static final String[] PUBLIC_MATCHERS_POST = {"/login", "/logout"};
@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");
}
private CsrfTokenRepository getCsrfTokenRepository() {
CookieCsrfTokenRepository tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
tokenRepository.setCookiePath("/");
return tokenRepository;
}
@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
builder.userDetailsService(clienteUserDetailsService)
.passwordEncoder(passwordEncoder());
}
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("**")
.allowedOrigins("http://localhost:4200", "https://dogwalk-teste.web.app")
.allowedMethods("POST, GET, PUT, OPTIONS, DELETE, PATCH")
.allowCredentials(true);
}
};
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
CorsFilter:
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorsFilter implements Filter {
@Context
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
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");
response.setHeader("Set-Cookie", "locale=pt-BR; HttpOnly; Secure; SameSite=None;");
chain.doFilter(req, res);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void destroy() {
}
}
在基本身份验证的情况下,响应是 flushed/committed 在控制器 returns 响应对象之后,在调用 SameSiteFilter#addSameSiteCookieAttribute 之前。
您需要在创建会话后立即包装请求并调整 cookie。您可以通过定义以下内容来实现它 类:
one bean (You can define it inside SecurityConfig if you want to hold everything in one place. I just put @Component annotation on it for brevity)
package com.dogwalk.dogwalk.config;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.web.firewall.FirewalledRequest;
import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.firewall.RequestRejectedException;
import org.springframework.stereotype.Component;
@Component
public class MyHttpFirewall implements HttpFirewall {
@Override
public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException {
return new RequestWrapper(request);
}
@Override
public HttpServletResponse getFirewalledResponse(HttpServletResponse response) {
return new ResponseWrapper(response);
}
}
first wrapper class
package com.dogwalk.dogwalk.config;
import java.util.Collection;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.http.HttpHeaders;
import org.springframework.security.web.firewall.FirewalledRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* Wrapper around HttpServletRequest that overwrites Set-Cookie response header and adds SameSite=None portion.
*/
public class RequestWrapper extends FirewalledRequest {
/**
* Constructs a request object wrapping the given request.
*
* @param request The request to wrap
* @throws IllegalArgumentException if the request is null
*/
public RequestWrapper(HttpServletRequest request) {
super(request);
}
/**
* Must be empty by default in Spring Boot. See FirewalledRequest.
*/
@Override
public void reset() {
}
@Override
public HttpSession getSession(boolean create) {
HttpSession session = super.getSession(create);
if (create) {
ServletRequestAttributes ra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (ra != null) {
overwriteSetCookie(ra.getResponse());
}
}
return session;
}
@Override
public String changeSessionId() {
String newSessionId = super.changeSessionId();
ServletRequestAttributes ra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (ra != null) {
overwriteSetCookie(ra.getResponse());
}
return newSessionId;
}
private void overwriteSetCookie(HttpServletResponse response) {
if (response != null) {
Collection<String> headers = response.getHeaders(HttpHeaders.SET_COOKIE);
boolean firstHeader = true;
for (String header : headers) { // there can be multiple Set-Cookie attributes
if (firstHeader) {
response.setHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=None")); // set
firstHeader = false;
continue;
}
response.addHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=None")); // add
}
}
}
}
second wrapper class
package com.dogwalk.dogwalk.config;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
/**
* Dummy implementation.
* To be aligned with RequestWrapper.
*/
public class ResponseWrapper extends HttpServletResponseWrapper {
/**
* Constructs a response adaptor wrapping the given response.
*
* @param response The response to be wrapped
* @throws IllegalArgumentException if the response is null
*/
public ResponseWrapper(HttpServletResponse response) {
super(response);
}
}
Finally you can remove obsolete SameSiteFilter as all the work will be done within RequestWrapper#overwriteSetCookie.
Pay attention that Postman doesn't render/support SameSite cookie attribute under Cookies section. You need to look at Set-Cookie response header or use curl.
Spring Boot 2.6.0 现在支持配置 SameSite cookie 属性:
通过属性配置
server.servlet.session.cookie.same-site=strict
通过代码配置
import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class MySameSiteConfiguration {
@Bean
public CookieSameSiteSupplier applicationCookieSameSiteSupplier() {
return CookieSameSiteSupplier.ofStrict();
}
}
我有一个 spring boot API 托管在 Heroku 上,当我尝试通过 Google Chrome 中的 Angular 应用访问它时(在Firefox 它工作正常)我面临以下问题:
似乎 JSESSIONID cookie 被阻止了,因为它没有设置为 SameSite=None。但是如何将其设置为 SameSite=None?
以下是我的配置类:
安全配置:
@Configuration
@EnableWebSecurity
@Order(SecurityProperties.DEFAULT_FILTER_ORDER)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private ClienteUserDetailsService clienteUserDetailsService;
private static final String[] PUBLIC_MATCHERS = {"/login", "/logout", "/error.html", "/error"};
private static final String[] PUBLIC_MATCHERS_GET = {"/login", "/logout", "/error.html", "/error"};
private static final String[] PUBLIC_MATCHERS_POST = {"/login", "/logout"};
@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");
}
private CsrfTokenRepository getCsrfTokenRepository() {
CookieCsrfTokenRepository tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
tokenRepository.setCookiePath("/");
return tokenRepository;
}
@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
builder.userDetailsService(clienteUserDetailsService)
.passwordEncoder(passwordEncoder());
}
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("**")
.allowedOrigins("http://localhost:4200", "https://dogwalk-teste.web.app")
.allowedMethods("POST, GET, PUT, OPTIONS, DELETE, PATCH")
.allowCredentials(true);
}
};
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
CorsFilter:
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorsFilter implements Filter {
@Context
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
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");
response.setHeader("Set-Cookie", "locale=pt-BR; HttpOnly; Secure; SameSite=None;");
chain.doFilter(req, res);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void destroy() {
}
}
在基本身份验证的情况下,响应是 flushed/committed 在控制器 returns 响应对象之后,在调用 SameSiteFilter#addSameSiteCookieAttribute 之前。
您需要在创建会话后立即包装请求并调整 cookie。您可以通过定义以下内容来实现它 类:
one bean (You can define it inside SecurityConfig if you want to hold everything in one place. I just put @Component annotation on it for brevity)
package com.dogwalk.dogwalk.config;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.web.firewall.FirewalledRequest;
import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.firewall.RequestRejectedException;
import org.springframework.stereotype.Component;
@Component
public class MyHttpFirewall implements HttpFirewall {
@Override
public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException {
return new RequestWrapper(request);
}
@Override
public HttpServletResponse getFirewalledResponse(HttpServletResponse response) {
return new ResponseWrapper(response);
}
}
first wrapper class
package com.dogwalk.dogwalk.config;
import java.util.Collection;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.http.HttpHeaders;
import org.springframework.security.web.firewall.FirewalledRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* Wrapper around HttpServletRequest that overwrites Set-Cookie response header and adds SameSite=None portion.
*/
public class RequestWrapper extends FirewalledRequest {
/**
* Constructs a request object wrapping the given request.
*
* @param request The request to wrap
* @throws IllegalArgumentException if the request is null
*/
public RequestWrapper(HttpServletRequest request) {
super(request);
}
/**
* Must be empty by default in Spring Boot. See FirewalledRequest.
*/
@Override
public void reset() {
}
@Override
public HttpSession getSession(boolean create) {
HttpSession session = super.getSession(create);
if (create) {
ServletRequestAttributes ra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (ra != null) {
overwriteSetCookie(ra.getResponse());
}
}
return session;
}
@Override
public String changeSessionId() {
String newSessionId = super.changeSessionId();
ServletRequestAttributes ra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (ra != null) {
overwriteSetCookie(ra.getResponse());
}
return newSessionId;
}
private void overwriteSetCookie(HttpServletResponse response) {
if (response != null) {
Collection<String> headers = response.getHeaders(HttpHeaders.SET_COOKIE);
boolean firstHeader = true;
for (String header : headers) { // there can be multiple Set-Cookie attributes
if (firstHeader) {
response.setHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=None")); // set
firstHeader = false;
continue;
}
response.addHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=None")); // add
}
}
}
}
second wrapper class
package com.dogwalk.dogwalk.config;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
/**
* Dummy implementation.
* To be aligned with RequestWrapper.
*/
public class ResponseWrapper extends HttpServletResponseWrapper {
/**
* Constructs a response adaptor wrapping the given response.
*
* @param response The response to be wrapped
* @throws IllegalArgumentException if the response is null
*/
public ResponseWrapper(HttpServletResponse response) {
super(response);
}
}
Finally you can remove obsolete SameSiteFilter as all the work will be done within RequestWrapper#overwriteSetCookie.
Pay attention that Postman doesn't render/support SameSite cookie attribute under Cookies section. You need to look at Set-Cookie response header or use curl.
Spring Boot 2.6.0 现在支持配置 SameSite cookie 属性:
通过属性配置
server.servlet.session.cookie.same-site=strict
通过代码配置
import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class MySameSiteConfiguration {
@Bean
public CookieSameSiteSupplier applicationCookieSameSiteSupplier() {
return CookieSameSiteSupplier.ofStrict();
}
}