【问题标题】:How to set SameSite=None in JSESSIONID Cookie如何在 JSESSIONID Cookie 中设置 SameSite=None
【发布时间】:2021-07-22 23:32:35
【问题描述】:

我在 Heroku 上托管了一个 Spring Boot API,当我尝试通过 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() {
    }
}

【问题讨论】:

  • 嗨@Andre!您是否可以提供一个小示例项目(您可以将其推送到 github),我可以使用它来重现问题?
  • 让我看看...
  • 你还没有上传 pom.xml / build.gradle 和所有的依赖定义到 GitHub。不幸的是,我无法构建或运行您的项目。能否请您上传其他必要的文件?
  • @EugeneMaysyuk 抱歉...文件已上传!
  • 我看到了问题。我需要一些时间来考虑正确的解决方法。

标签: java spring-boot spring-security spring-session jsessionid


【解决方案1】:

如果是基本身份验证,则在控制器返回响应对象之后,在调用 SameSiteFilter#addSameSiteCookieAttribute 之前立即刷新/提交响应。

您需要在会话创建后立即包装请求并调整 cookie。您可以通过定义以下类来实现它:

一个bean(如果你想把所有东西都放在一个地方,你可以在SecurityConfig中定义它。为了简洁,我只是在上面放了@Component注解)

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);
    }

}

第一个包装类

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
            }
        }
    }
}

第二个包装类

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);
    }
}

最后,您可以删除过时的 SameSiteFilter,因为所有工作都将在 RequestWrapper#overwriteSetCookie 中完成。

请注意 Postman 在 Cookies 部分下不呈现/支持 SameSite cookie 属性。您需要查看 Set-Cookie 响应标头或使用 curl。

【讨论】:

  • 如果您将我添加为合作者,我可以推送我的本地更改并为您的存储库创建 PR。我的 GitHub:@emaysyuk
  • @Andre 你需要用 \@Component 注释 MyHttpFirewall,否则请求/响应将不会被包装并且 SameSite=None 不会被添加
  • 你说得对,尤金! OMG 我在创建 MyHttpFirewall 类时错过了@Component。现在它在 google chrome 浏览器中运行良好。非常感谢你帮助我的朋友,你是男人!!!上帝保佑你!
  • 很高兴能帮上忙
  • @EugeneMaysyuk 这对春季安全有什么作用?我在使用基本身份验证时遇到了麻烦!而且我真的不知道这是如何工作的......我已经尝试过这个解决方案,在包装器上放了一些日志,但没有显示。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-01-04
  • 1970-01-01
  • 2020-09-01
  • 2019-05-14
  • 2019-12-18
  • 2020-05-17
  • 2020-05-22
相关资源
最近更新 更多