如何为jsessionid cookie启用samesite

25

如何在运行于WildFly上的Web应用程序中启用SameSite?查看了standalone.xml,但是没有找到合适的标签。

<servlet-container name="default">
    <session-cookie http-only="true" secure="true"/>
    <jsp-config/>
</servlet-container>

1
您是否考虑过使用Spring Session 2.1.0.RELEASE版本支持的samesite cookie?请参见https://github.com/spring-projects/spring-session/blob/2.1.0.RELEASE/spring-session-core/src/main/java/org/springframework/session/web/http/DefaultCookieSerializer.java#L85 - snieguu
请查看此链接,其中使用了GenericFilterBean / 临时重定向请求来解决类似问题:https://dev59.com/lVIG5IYBdhLWcg3wlCK8#63939775 - ThilankaD
我在使用已接受的解决方案时遇到了麻烦,因为在任何调用中都没有“Set-Cookie”头。因此,我尝试了来自StackOverflow的另一个解决方案,当然需要根据需要更改标志:向现有的Jsessionid cookie添加标志 - Annanraen
6个回答

33

Spring Boot 2.6.0

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 2.5.0及以下版本

Spring Boot 2.5.0不支持SameSite cookie属性,并且没有启用它的设置。

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

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

方法#1(使用自定义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):

这种方法不适用于基本身份验证。 在基本身份验证情况下,响应会在控制器返回响应对象后立即被刷新/提交,然后才调用AuthenticationSuccessHandlerImpl#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() {

    }
}

第四种方法(如果您正在使用Tomcat 9.0.21 / Tomcat 8.5.42或以上版本)

在您的Web应用程序中,创建一个META-INF文件夹,在其中创建一个context.xml文件,并添加以下内容:

<Context>
   <CookieProcessor sameSiteCookies="strict" />
</Context>

从Tomcat 9.0.28 / Tomcat 8.5.48开始,可以设置SameSite为none。

有关更多详细信息,请参见此pull request

Demo项目

您可以在GitHub上查看此演示项目,以了解前三种方法的配置详细信息。

SecurityConfig包含所有必要的配置。

使用addHeader并不保证可行,因为Servlet容器基本上管理Session和Cookie的创建。例如,在响应体中返回JSON时,第二个和第三个方法无法工作,因为应用服务器会在刷新响应期间覆盖Set-Cookie头。但是,在成功身份验证后将用户重定向到另一页的情况下,第二个和第三个方法将有效。

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


1
这个问题是如何在修改cookie之前运行doFilter?我尝试了很多方法,但无论我做什么都无法修改我的cookie。 - Lightheaded
好的,我想我的问题出在别处(比如Tomcat覆盖了cookie)。谢谢! - Lightheaded
@Lightheaded,你能否描述一下在你的情况下发生了什么,Tomcat是如何覆盖“Set-Cookie”头部的?如果你有更多的信息,我可能需要更新帖子。顺便问一下,你有一些示例应用程序,这样我就可以重现你所面临的问题吗? - Eugene Maysyuk
1
@Lightheaded,你能否创建一个样例项目来重现你的问题并上传到Github上吗?我可以看一下并尝试帮助你。 - Eugene Maysyuk
1
HTTP请求中首先发送头部信息,因此如果响应已经(部分)刷新到客户端,则此方法将无法正常工作。 - cocorossello
显示剩余15条评论

9

一种解决方法是通过使用另一个属性(例如comment)将SameSite设置入cookie中:

<servlet-container name="default">
    <jsp-config/>
    <session-cookie comment="; SameSite=None"/>
    <websockets/>
</servlet-container>

但是,因为Undertow在使用版本0或版本1的cookie时会引用注释(和其他)值,所以JBoss/WildFly需要运行时设置系统属性io.undertow.cookie.DEFAULT_ENABLE_RFC6265_COOKIE_VALIDATIONtrue

 ./bin/standalone.sh -Dio.undertow.cookie.DEFAULT_ENABLE_RFC6265_COOKIE_VALIDATION=true

这将为您提供所需的结果: cookies 这种方法显然有些投机,完全依赖于Undertow实现细节,因此我建议在Web服务器或负载均衡器级别上进行配置。

1
或者您可以在子系统->服务器->default-server->http-listener标签中设置属性rfc6265-cookie-validation=true<http-listener name="default" socket-binding="http" max-post-size="10485760000" rfc6265-cookie-validation="true" redirect-socket="https" enable-http2="true"/> - Rahil Husain

8

对于当前最新版本的Spring Boot:

如果您没有最新的spring-boot-starter-tomcat,请检查SameSiteCookies枚举的值UNSET,如果缺少值,则需要更新版本,因为它会跳过SameSite=None的值。

@Component
public class SameSiteTomcatCookieProcessorCustomizationBean implements WebServerFactoryCustomizer<TomcatServletWebServerFactory>
{
    @Override
    public void customize(TomcatServletWebServerFactory server) {

        server.getTomcatContextCustomizers().add(new TomcatContextCustomizer()
        {
            @Override
            public void customize(Context context)
            {
                Rfc6265CookieProcessor cookieProcessor = new Rfc6265CookieProcessor();
                cookieProcessor.setSameSiteCookies("None");
                context.setCookieProcessor(cookieProcessor);
            }
        });
    }
}

1
这个回答帮了我!我们不使用spring-session,所以其他的答案并不适用。 - Anna Ira Hurnaus

2

如果您使用的是WildFly 19或更高版本,则推荐使用undertow-handlers.conf定义SameSite策略。这非常灵活,因为您可以定义Web上下文,在该上下文下将使用SameSite策略,并为cookie定义一个正则表达式模式。 例如:

path(/app2)->samesite-cookie(mode=Lax, cookie-pattern=abc*)

另一方面,对于Tomcat应用程序,您可以添加一个带有sameSiteCookies属性的META-INF/context.xml文件,例如:

<Context>
   <CookieProcessor sameSiteCookies="strict" />
</Context>

一些参考资料: https://github.com/apache/tomcat/pull/162 如何在Web应用程序中设置SameSite属性

2

Wildfly 19.1.0及更高版本的解决方案:

$ cat src/main/webapp/WEB-INF/undertow-handlers.conf
samesite-cookie(mode=Lax)

资源:https://www.wildfly.org/news/2020/05/04/WildFly-1910-Released/

本文介绍了 WildFly 19.1.0 版本的发布。WildFly 是一个适用于 Java 平台的开源应用服务器,具有高度可伸缩性、灵活性和可扩展性。新版本增加了对 JavaEE 8 的支持,并提供了一些改进和修复了一些 bug。


1
这个可以(自 WildFly 19.1 起)通过 standalone.conf 或 CLI 进行配置。请参见我的回答以获取类似问题的解决方案。 - Martin Höller

2

我的解决方法,在JBoss EAP 7.2中有效,是使用自定义处理程序。我将其用作全局处理程序,但您也可以在jboss-web.xml中使用它。您需要调整cookie实现,因为undertow仅允许Strict或Lax的samesite(如果您使用cookie.setSameSiteMode("None"),它会抛出"'UT000162:Same-site attribute None is invalid. It must be Strict or Lax"')。

import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.Cookie;
import java.lang.reflect.Proxy;
import java.util.Map;

public class CookieSameSiteHandler implements HttpHandler
{
   private  HttpHandler next;

   public CookieSameSiteHandler(HttpHandler next){
      this.next = next;
   }

   @Override
   public void handleRequest(final HttpServerExchange exchange)
      throws Exception
   {
      exchange.addResponseCommitListener(serverExchange -> {
         for (Map.Entry<String, Cookie> responcecookie : serverExchange.getResponseCookies().entrySet()){
            serverExchange.getResponseCookies().replace(responcecookie.getKey(), proxyCookie(responcecookie.getValue()));
         }
      });
      next.handleRequest(exchange);
   }

   private Cookie proxyCookie(Cookie cookie)
   {
      return (Cookie)Proxy.newProxyInstance(
         cookie.getClass().getClassLoader(),
         cookie.getClass().getInterfaces(),
         (proxy, method, args) -> {
            if ("isSameSite".equals(method.getName())){
               return true;
            }
            if ("getSameSiteMode".equals(method.getName()) && cookie.getSameSiteMode() == null){
               return "None";
            }
            if ("isSecure".equals(method.getName()) && cookie.getSameSiteMode() == null){
               return true;
            }
            return method.invoke(cookie, args);
         });
   }
}

处理程序配置:

<subsystem xmlns="urn:jboss:domain:undertow:7.0" default-virtual-host="default-host">
    <buffer-cache name="default"/>
    <server name="default-server" default-host="default-host">
        ...
        <host name="default-host" alias="localhost,example.com">
            ...
            <filter-ref name="cookiehandler"/>
            ...
        </host>
    </server>
    ...
    <filters>
        <filter class-name="nl.myownstuff.handler.CookieSameSiteHandler" module="nl.myownstuff.undertow" name="cookiehandler"/>
    </filters>
</subsystem>

从WildFly 19.1及以上版本开始,您不再需要编写自己的类。这可以通过在standalone.conf中配置expression-filter来实现,如this answer所述。 - Martin Höller

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接