Java 应用程序中的 SameSite cookie

SameSite cookie in Java application

您是否知道任何允许为 cookie 设置自定义标志的 Java cookie 实现,例如 SameSite=strict?似乎 javax.servlet.http.Cookie 有一组严格限制的可以添加的标志。

我不是 JEE 专家,但我认为因为 cookie 属性 是一个有点新的发明,所以你不能指望它出现在 Java EE 7 接口或实现中。 Cookie class 似乎缺少通用属性的 setter。但不是通过

将 cookie 添加到您的 HttpServletResponse
response.addCookie(myCookie)

你可以简单地通过

设置相应的 HTTP header 字段
response.setHeader("Set-Cookie", "key=value; HttpOnly; SameSite=strict")

更新: 感谢 @mwyrzyk 指出 setHeader() 覆盖了所有现有的 header同名。因此,如果您的响应中已经有其他 Set-Cookie header,当然您会使用具有相同参数的 addHeader()

如果您不想更新所有代码,您也可以使用 Apache 或 Nginx 配置(或您正在使用的任何其他 HTTP server/proxy)通过一行配置来实现相同目的

1 使用 Apache 配置设置 SameSite cookie

您可以将以下行添加到您的 Apache 配置中

Header always edit Set-Cookie (.*) "; SameSite=Lax"

这将使用 SameSite=Lax 标志更新您所有的 cookie

在此处查看更多信息:https://blog.giantgeek.com/?p=1872

2 使用 Nginx 配置设置 SameSite cookie

location / {
    # your usual config ...
    # hack, set all cookies to secure, httponly and samesite (strict or lax)
    proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict";
}

这里也一样,这也会使用 SameSite=Lax 标志更新您所有的 cookie

在此处查看更多内容:https://serverfault.com/questions/849888/add-samesite-to-cookies-using-nginx-as-reverse-proxy

截至今天 (24.01.20) servlet-api 不允许设置 sameSite 属性到 cookie。顺便说一句,有一张正在进行的工单 (LINK) 将发布新的(5.0 或 5.1 servlet-api)。

方案一:你不急,可以等servlet-api版本,其中Cookieclass和SessionCookieConfig class 有专门的方法来设置 sameSite 属性。

选项 2: 您使用的是旧版本的 servlet-api(例如 3.1),因此是旧版本的 Tomcat(例如我现在有现在的情况)。这意味着即使社区发布 servlet-api 支持 sameSite,您也不能立即更新您的版本,因为更新几个主要版本的风险太大。
在这种情况下,我们找到了解决方案。
Tomcat中有一个Cookie Processor ComponentLINK,其中

The CookieProcessor element represents the component that parses received cookie headers into javax.servlet.http.Cookie objects accessible through HttpServletRequest.getCookies() and converts javax.servlet.http.Cookie objects added to the response through HttpServletResponse.addCookie() to the HTTP headers returned to the client.

这个处理器的使用非常简单。 context.xml 内部:

<Context>
    ...
    <CookieProcessor sameSiteCookies="none"/>
</Context>

在这种情况下,使用处理器的默认实现 (org.apache.tomcat.util.http.Rfc6265CookieProcessor),但您可以在 CookieProcessor 属性 className 中指定任何其他实现。

Jetty 服务器版本 9.4.26.v20200117 允许在 cookie 上设置 SameSite 属性。我不得不四处挖掘,但这很有效。

import static org.eclipse.jetty.http.HttpCookie.SAME_SITE_STRICT_COMMENT;

...

Cookie cookie = new Cookie("my-cookie", "some-value");
cookie.setMaxAge(120); // age in seconds
cookie.setSecure(true);
cookie.setHttpOnly(true);
cookie.setComment(SAME_SITE_STRICT_COMMENT);

response.addCookie(cookie);

jetty 服务器的 Response 对象上的 addCookie() 方法检查评论以添加 SameSite 属性。

如果您有现有代码,毫无疑问您已经使用了 java servlet Cookie object。我们当然有,所以我们想要破坏性最小的选择。 @kriegaex 的回答简洁明了,但基本上是对 cookie 进行硬编码并且不重用 cookie object。为了扩展他的回答,我们编写了这个函数来处理相同的站点功能,同时维护现有的 Cookie object 功能。此答案旨在用于您需要在响应 object 中添加多个 cookie 的情况,而无需更改可能已经在 headers 上的现有 cookie。另一种选择当然是编写一个新的 cookie class 并扩展功能,但这需要对现有代码进行比我们在这里提出的更多的更改。

请注意,使用此解决方案,只需更改一行现有代码(每个 cookie)即可添加相同的网站功能。

示例用法:

// Existing code that doesn't change:   
Cookie cookie1=new Cookie("cookie1",Util.encodeURL(id));
cookie1.setHttpOnly(false);
cookie1.setPath("/");

Cookie cookie2=new Cookie("cookie2",Util.encodeURL(id));
cookie2.setHttpOnly(false);
cookie2.setPath("/");

// Old Code that is replaced by new code
// httpResponse.addCookie(cookie1);
// httpResponse.addCookie(cookie2);

// New Code - see static helper class below
HttpService.addCookie(httpResponse, cookie1, "none");
HttpService.addCookie(httpResponse, cookie2, "Strict");

使用 cURL 时的响应示例 headers:

< HTTP/1.1 200 OK
< Connection: keep-alive
< X-Powered-By: Undertow/1
< Set-Cookie: cookie1=f871c026e8eb418c9c612f0c7fe05b08; path=/; SameSite=none; secure
< Set-Cookie: cookie2=51b405b9487f4487b50c80b32eabcc24; path=/; SameSite=Strict; secure
< Server: WildFly/9
< Transfer-Encoding: chunked
< Content-Type: image/png
< Date: Tue, 10 Mar 2020 01:55:37 GMT

最后,静态助手 class:

public class HttpService {
    private static final FastDateFormat expiresDateFormat= FastDateFormat.getInstance("EEE, dd MMM yyyy HH:mm:ss zzz", TimeZone.getTimeZone("GMT"));


    public static void addCookie(HttpServletResponse response, Cookie cookie, String sameSite) {

        StringBuilder c = new StringBuilder(64+cookie.getValue().length());

        c.append(cookie.getName());
        c.append('=');
        c.append(cookie.getValue());

        append2cookie(c,"domain",   cookie.getDomain());
        append2cookie(c,"path",     cookie.getPath());
        append2cookie(c,"SameSite", sameSite);

        if (cookie.getSecure()) {
            c.append("; secure");
        }
        if (cookie.isHttpOnly()) {
            c.append("; HttpOnly");
        }
        if (cookie.getMaxAge()>=0) {
            append2cookie(c,"Expires", getExpires(cookie.getMaxAge()));
        }

        response.addHeader("Set-Cookie", c.toString());
    }

    private static String getExpires(int maxAge) {
        if (maxAge<0) {
            return "";
        }
        Calendar expireDate = Calendar.getInstance();
        expireDate.setTime(new Date());
        expireDate.add(Calendar.SECOND,maxAge);

        return expiresDateFormat.format(expireDate);
    }

    private static void append2cookie(StringBuilder cookie, String key, String value) {
        if (key==null || 
                value==null || 
                key.trim().equals("") 
                || value.trim().equals("")) {
            return;
        }

        cookie.append("; ");
        cookie.append(key);
        cookie.append('=');
        cookie.append(value);
    }
}

我发现 return 成功创建的 cookie 没有被 "Header edit" 或 "Header always edit" 更改。显然 apache 有两桶 cookie - 请参阅

对我有用的是

Header onsuccess edit Set-Cookie (.*) "; SameSite=Lax"

我尝试了列出的使用 javax.servlet.http.Cookie 设置 SameSite=strict 属性的解决方案,但其中 none 有效。

但是,这种方式对我有用,使用 javax.servlet.http.Cookie (JRE 1.8 + JBOSS 7.X) :

Cookie cookie = new Cookie(name, value);
path = path.concat("SameSite=Strict;");
cookie.setPath(path);

就是这样。测试于

  • Google Chrome 版本 81.0.4044.129(正式版)(64 位)
  • Microsoft Edge 版本 81.0.416.68(正式版)(64 位)
  • Firefox 75.0(64 位)

如果使用 spring boot with Tom cat 那么这已经在另一个问题中得到了回答。 总之,在 tom cat 配置上设置属性。这是全球性的,所有 cookie 都将启用相同的站点。 (来自另一个问题)

@Configuration
public class MvcConfiguration implements WebMvcConfigurer {

  @Bean
  public TomcatContextCustomizer sameSiteCookiesConfig() {
    return context -> {
        final Rfc6265CookieProcessor cookieProcessor = new Rfc6265CookieProcessor();
        cookieProcessor.setSameSiteCookies(SameSiteCookies.NONE.getValue());
        context.setCookieProcessor(cookieProcessor);
    };
  }

不使用 spring 引导或 spring 会话的解决方案。

有关解决方案的更多详细信息 Samesite for jessessionId cookie can be set only from response

        package com.cookie.example.filters.cookie;


  import com.google.common.net.HttpHeaders;
  import org.apache.commons.collections.CollectionUtils;
  import org.apache.commons.lang3.StringUtils;
  import org.springframework.beans.factory.InitializingBean;
  import org.springframework.web.filter.DelegatingFilterProxy;

  import javax.annotation.Nonnull;
  import javax.servlet.*;
  import javax.servlet.http.HttpServletRequest;
  import javax.servlet.http.HttpServletResponse;
  import javax.servlet.http.HttpServletResponseWrapper;
  import java.io.IOException;
  import java.io.PrintWriter;
  import java.util.Collection;
  import java.util.Collections;
  import java.util.List;

  /**
   * Implementation of an HTTP filter {@link Filter} which which allow customization of {@literal Set-Cookie} header.
   * customization is delegated to implementations of {@link CookieHeaderCustomizer}
   */
  public class CookieHeaderCustomizerFilter extends DelegatingFilterProxy implements InitializingBean {

    private final List<CookieHeaderCustomizer> cookieHeaderCustomizers;

    @Override
    public void afterPropertiesSet() throws ServletException {
      super.afterPropertiesSet();
      if(CollectionUtils.isEmpty(cookieHeaderCustomizers)){
        throw new IllegalArgumentException("cookieHeaderCustomizers is mandatory");
      }
    }

    public CookieHeaderCustomizerFilter(final List<CookieHeaderCustomizer> cookieHeaderCustomizers) {
      this.cookieHeaderCustomizers = cookieHeaderCustomizers;
    }

    public CookieHeaderCustomizerFilter() {
      this.cookieHeaderCustomizers = Collections.emptyList();
    }


    /** {@inheritDoc} */
    public void destroy() {
    }

    /** {@inheritDoc} */
    public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
      throws IOException, ServletException {

      if (!(request instanceof HttpServletRequest)) {
        throw new ServletException("Request is not an instance of HttpServletRequest");
      }

      if (!(response instanceof HttpServletResponse)) {
        throw new ServletException("Response is not an instance of HttpServletResponse");
      }

      chain.doFilter(request, new CookieHeaderResponseWrapper((HttpServletRequest) request, (HttpServletResponse)response ));

    }


    /**
     * An implementation of the {@link HttpServletResponse} which customize {@literal Set-Cookie}
     */
    private class CookieHeaderResponseWrapper extends HttpServletResponseWrapper{

      @Nonnull private final HttpServletRequest request;

      @Nonnull private final HttpServletResponse response;


      public CookieHeaderResponseWrapper(@Nonnull final HttpServletRequest req, @Nonnull final HttpServletResponse resp) {
        super(resp);
        this.request = req;
        this.response = resp;

      }

      /** {@inheritDoc} */
      @Override
      public void sendError(final int sc) throws IOException {
        applyCustomizers();
        super.sendError(sc);
      }

      /** {@inheritDoc} */
      @Override
      public PrintWriter getWriter() throws IOException {
        applyCustomizers();
        return super.getWriter();
      }

      /** {@inheritDoc} */
      @Override
      public void sendError(final int sc, final String msg) throws IOException {
        applyCustomizers();
        super.sendError(sc, msg);
      }

      /** {@inheritDoc} */
      @Override
      public void sendRedirect(final String location) throws IOException {
        applyCustomizers();
        super.sendRedirect(location);
      }

      /** {@inheritDoc} */
      @Override
      public ServletOutputStream getOutputStream() throws IOException {
        applyCustomizers();
        return super.getOutputStream();
      }

      private void applyCustomizers(){

        final Collection<String> cookiesHeaders = response.getHeaders(HttpHeaders.SET_COOKIE);

        boolean firstHeader = true;

        for (final String cookieHeader : cookiesHeaders) {

          if (StringUtils.isBlank(cookieHeader)) {
            continue;
          }

          String customizedCookieHeader = cookieHeader;

          for(CookieHeaderCustomizer cookieHeaderCustomizer : cookieHeaderCustomizers){

            customizedCookieHeader = cookieHeaderCustomizer.customize(request, response, customizedCookieHeader);

          }

          if (firstHeader) {
            response.setHeader(HttpHeaders.SET_COOKIE,customizedCookieHeader);
            firstHeader=false;
          } else {
            response.addHeader(HttpHeaders.SET_COOKIE, customizedCookieHeader);
          }

        }

      }

    }

  }



  /**
   * Implement this interface and inject add it to {@link SameSiteCookieHeaderCustomizer}
   */
  public interface CookieHeaderCustomizer {
    String customize(@Nonnull final HttpServletRequest request, @Nonnull final HttpServletResponse response, @Nonnull final String cookieHeader);
  }


    package com.cookie.example.filters.cookie;

      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;

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

  /**
   *Add SameSite attribute if not already exist
   *SameSite attribute value is defined by property "cookie.sameSite"
   */
  public class SameSiteCookieHeaderCustomizer implements CookieHeaderCustomizer {

    private static final Logger LOGGER = LoggerFactory.getLogger(SameSiteCookieHeaderCustomizer.class);

    private static final String SAME_SITE_ATTRIBUTE_NAME ="SameSite";

    private static final String SECURE_ATTRIBUTE_NAME="Secure";

    private final SameSiteValue sameSiteValue;

    public SameSiteCookieHeaderCustomizer(SameSiteValue sameSiteValue) {
      this.sameSiteValue = sameSiteValue;
    }


    @Override
    public String customize(@Nonnull final HttpServletRequest request, @Nonnull final HttpServletResponse response, @Nonnull final String cookieHeader) {
      StringBuilder sb = new StringBuilder(cookieHeader);
      if (!cookieHeader.contains(SAME_SITE_ATTRIBUTE_NAME)) {
        sb.append("; ").append(SAME_SITE_ATTRIBUTE_NAME).append("=").append(sameSiteValue.value);
      }
      if(SameSiteValue.None == sameSiteValue && !cookieHeader.contains(SECURE_ATTRIBUTE_NAME)){
        sb.append("; ").append(SECURE_ATTRIBUTE_NAME);
      }
      return sb.toString();
    }

    public enum SameSiteValue{

      /**
       * Send the cookie for 'same-site' requests only.
       */
      Strict("Strict"),
      /**
       * Send the cookie for 'same-site' requests along with 'cross-site' top
       * level navigations using safe HTTP methods (GET, HEAD, OPTIONS, and TRACE).
       */
      Lax("Lax"),
      /**
       * Send the cookie for 'same-site' and 'cross-site' requests.
       */
      None("None");

      /** The same-site attribute value.*/
      private String value;

      /**
       * Constructor.
       *
       * @param attrValue the same-site attribute value.
       */
      SameSiteValue(@Nonnull final String attrValue) {
        value = attrValue;
      }

      /**
       * Get the same-site attribute value.
       *
       * @return Returns the value.
       */
      public String getValue() {
        return value;
      }

    }

  }

ravinder5 确实实现了这个并开源了它:CookieHeader

示例用法:

import com.tgt.egs.auth.cookie.CookieHeader;
...

CookieHeader.createSetCookieHeader(cookieName, cookieValue, domain, path, sameSite, secure, httpOnly, expiry);
        

如果您碰巧使用 Spring 框架,您可以利用 ResponseCookie class。例如:

final ResponseCookie responseCookie = ResponseCookie
        .from("<my-cookie-name>", "<my-cookie-value-here>")
        .secure(true)
        .httpOnly(true)
        .path("/auth")
        .maxAge(12345)
        .sameSite("Lax")
        .build();
response.addHeader(HttpHeaders.SET_COOKIE, responseCookie.toString());

Disclamer:标志及其值仅作为 class' API.

的示例提供