20

是否可以在 Spring Boot 中设置Same-Site Cookie标志?

我在 Chrome 中的问题:

与http://google.com/上的跨站点资源关联的 cookie 设置为没有该SameSite属性。未来版本的 Chrome 将仅提供带有跨站点请求的 cookie,前提是它们使用SameSite=None和设置Secure。您可以在应用程序>存储>Cookies 下的开发人员工具中查看 cookie,并在 https://www.chromestatus.com/feature/5088147346030592https://www.chromestatus.com/feature/5633521622188032中查看更多详细信息。


如何解决这个问题呢?
4

7 回答 7

10

这是 Spring Security 的一个未解决问题 ( https://github.com/spring-projects/spring-security/issues/7537 )

正如我在 Spring-Boot ( 2.1.7.RELEASE) 中检查的那样,默认情况下它使用DefaultCookieSerializer带有sameSite默认为Lax.

您可以通过以下代码在应用程序启动时对其进行修改。

注意:这是一个 hack,直到真正的修复(配置)在下一个春季发布时公开。

@Component
@AllArgsConstructor
public class SameSiteInjector {

  private final ApplicationContext applicationContext;

  @EventListener
  public void onApplicationEvent(ContextRefreshedEvent event) {
    DefaultCookieSerializer cookieSerializer = applicationContext.getBean(DefaultCookieSerializer.class);
    log.info("Received DefaultCookieSerializer, Overriding SameSite Strict");
    cookieSerializer.setSameSite("strict");
  }
}
于 2019-10-21T13:50:51.217 回答
6

当前版本的 Spring Boot (2.5.0-SNAPSHOT) 不支持 SameSite cookie 属性并且没有启用它的设置。

Java Servlet 4.0 规范不支持 SameSite cookie 属性。您可以通过打开javax.servlet.http.Cookie java 类来查看可用属性。

但是,有几个解决方法。您可以手动覆盖 Set-Cookie 属性。

第一种方法(使用自定义 Spring HttpFirewall)和围绕请求的包装器:

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

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

package hello.approach1;

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 CustomHttpFirewall 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 hello.approach1;

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 hello.approach1;

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

第二种方法(使用 Spring 的 AuthenticationSuccessHandler):

此方法不适用于基本身份验证。在基本身份验证的情况下,在控制器返回响应对象之后,在调用 SameSiteFilter#addSameSiteCookieAttribute 之前立即刷新/提交响应。

package hello.approach2;

import java.io.IOException;
import java.util.Collection;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.http.HttpHeaders;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        addSameSiteCookieAttribute(response);    // add SameSite=strict to Set-Cookie attribute
        response.sendRedirect("/hello"); // redirect to hello.html after success auth
    }

    private void addSameSiteCookieAttribute(HttpServletResponse response) {
        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=Strict"));
                firstHeader = false;
                continue;
            }
            response.addHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=Strict"));
        }
    }
}

第三种方法(使用 javax.servlet.Filter):

此方法不适用于基本身份验证。在基本身份验证的情况下,在控制器返回响应对象之后,在调用 SameSiteFilter#addSameSiteCookieAttribute 之前立即刷新/提交响应。

package hello.approach3;

import java.io.IOException;
import java.util.Collection;

import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;

import org.springframework.http.HttpHeaders;

public class SameSiteFilter implements javax.servlet.Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        chain.doFilter(request, response);
        addSameSiteCookieAttribute((HttpServletResponse) response); // add SameSite=strict cookie attribute
    }

    private void addSameSiteCookieAttribute(HttpServletResponse response) {
        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=Strict"));
                firstHeader = false;
                continue;
            }
            response.addHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=Strict"));
        }
    }

    @Override
    public void destroy() {

    }
}

您可以查看GitHub 上的这个演示项目,了解有关 org.springframework.security.web.authentication.AuthenticationSuccessHandler 或 javax.servlet.Filter 配置的更多详细信息。

SecurityConfig包含所有必要的配置。

使用 addHeader 不能保证工作,因为基本上 Servlet 容器管理 Session 和 Cookie 的创建。例如,如果您在响应正文中返回 JSON,则第二种和第三种方法将不起作用,因为应用程序服务器将在刷新响应期间覆盖 Set-Cookie 标头。但是,在成功验证后将用户重定向到另一个页面的情况下,第二种和第三种方法将起作用。

请注意 Postman 不呈现/支持 Cookies 部分下的 SameSite cookie 属性(至少在撰写本文时)。您可以查看 Set-Cookie 响应标头或使用 curl 查看是否添加了 SameSite cookie 属性。

于 2021-05-03T08:56:49.097 回答
4

从 spring boot 版本 2.6.+ 开始,您可以通过编程方式或通过配置文件指定您的 samesite cookie。

Spring Boot 2.6.0 文档

如果您想通过配置文件将 samesite 设置为 lax,则:

server.servlet.session.cookie.same-site=lax

或以编程方式

@Configuration
public class MySameSiteConfiguration {

    @Bean
    public CookieSameSiteSupplier applicationCookieSameSiteSupplier() {
        return CookieSameSiteSupplier.ofLax();
    }

}
于 2022-01-12T09:23:30.877 回答
2

自上次更新以来,chrome 也开始向我显示该消息。不是关于 spring 的真正答案,但您可以将 cookie 标志添加到会话的标题中。就我而言,由于我使用的是 Spring Security,因此我打算在用户登录时添加它,因为我已经在操纵会话以添加身份验证数据。

有关更多信息,请查看类似主题的答案:https ://stackoverflow.com/a/43250133

要在用户登录后立即添加会话标头,您可以将代码基于此主题(通过创建实现 AuthenticationSuccessHandler 的 spring 组件):Spring Security。身份验证后重定向到受保护的页面

于 2019-10-15T12:31:50.733 回答
1

对我来说,以上都不起作用。我的问题是,登录后,使用本文中提到的其他方法创建的 SameSite 标志被重定向机制简单地忽略了。

在我们的 spring boot 2.4.4 应用程序中,我设法使用自定义 SameSiteHeaderWriter 完成了它:

import org.springframework.security.web.header.HeaderWriter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;

import static javax.ws.rs.core.HttpHeaders.SET_COOKIE;


/**
 * This header writer just adds "SameSite=None;" to the Set-Cookie response header
 */
public class SameSiteHeaderWriter implements HeaderWriter {

    private static final String SAME_SITE_NONE = "SameSite=None";

    private static final String SECURE = "Secure";

    @Override
    public void writeHeaders(HttpServletRequest request, HttpServletResponse response) {

        if (response.containsHeader(SET_COOKIE)) {

            var setCookie = response.getHeader(SET_COOKIE);
            var toAdd = new ArrayList<String>();
            toAdd.add(setCookie);

            if (! setCookie.contains(SAME_SITE_NONE)) {
                toAdd.add(SAME_SITE_NONE);
            }

            if (! setCookie.contains(SECURE)) {
                toAdd.add(SECURE);
            }

            response.setHeader(SET_COOKIE, String.join("; ", toAdd));

        }
    }

}

然后在我的 WebSecurityConfigurerAdapter#configure 中,我刚刚将此标头编写器添加到列表中,使用:

if (corsEnabled) {
            httpSecurity = httpSecurity
                        .cors()
                    .and()
                        .headers(configurer -> {
                            configurer.frameOptions().disable();
                            configurer.addHeaderWriter(new SameSiteHeaderWriter());
                        });
        }

用户必须知道风险,才能在我们的应用程序中明确启用此功能。

只是认为这可能对将来的某人有所帮助。

于 2021-09-23T07:02:28.407 回答
0

从 Spring Boot 2.6.0 开始,这现在是可能的并且很容易:

import org.springframework.http.ResponseCookie;
ResponseCookie springCookie = ResponseCookie.from("refresh-token", "000")
  .sameSite("Strict")
  .build();

并将其返回 a ResponseEntity,可能是这样的:

ResponseEntity
    .ok()
    .header(HttpHeaders.SET_COOKIE, springCookie.toString())
    .build();
于 2022-01-15T09:38:58.327 回答
-2

按照文档解决此问题: https ://github.com/GoogleChromeLabs/samesite-examples

它有不同语言的例子

于 2019-11-06T05:21:01.020 回答