Java 应用程序中的 SameSite cookie

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

81603 次浏览

我不是 JEE 专家,但是我认为,因为 cookie 属性是一个新发明,所以不能指望它出现在 JavaEE7接口或实现中。Cookie类似乎缺少泛型属性的 setter。但是,而不是添加饼干到您的 HttpServletResponse通过

response.addCookie(myCookie)

您可以通过以下方法简单地设置相应的 HTTP 头字段

response.setHeader("Set-Cookie", "key=value; HttpOnly; SameSite=strict")

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

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

1使用 Apache 配置设置 SameSite cookie

您可以在 Apache 配置中添加以下代码行

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

这将更新所有的饼干与 SameSite=Lax标志

点此查看更多内容: 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";
}

这里也一样,这也将更新您的所有 Cookie 与 SameSite=Lax标志

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

从今天起(24.01.20) servlet-api不允许将 sameSite属性设置为 cookie。顺便说一下,有一个正在进行的票(链接) ,将发布一个新的(5.0或5.1 servlet-api)。

选项1: 您并不着急,可以等待 servlet-api版本,其中 Cookie类和 SessionCookieConfig类都有专用的方法来设置 sameSite属性。

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

CookieProcessor 元素表示将接收到的 cookie 头解析为 javax.servlet.http 的组件。可以通过 HttpServletRequest.getCookies ()访问 Cookie 对象并转换 javax.servlet.http。Cookie 对象通过 HttpServletResponse.addCookie ()添加到返回给客户端的 HTTP 头的响应中。

这个处理器的用法非常简单:

<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 对象。我们当然有,所以我们想要最少干扰的选择。@ kriegaex 的回答简洁明了,但基本上是硬编码 cookie,并且不重用 cookie 对象。为了扩展他的答案,我们编写了这个函数来处理相同的站点功能,同时维护现有的 Cookie 对象功能。此答案适用于需要在响应对象上添加多个 Cookie 的情况,而不需要对可能已经在标题上的现有 Cookie 进行更改。当然,另一种选择是编写一个新的 cookie 类并扩展其功能,但是这需要对现有代码进行更多的更改。

注意,在这个解决方案中,为了添加相同的站点功能,只对现有代码(每个 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 时的响应头示例:

< 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

最后,静态 helper 类:

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

我发现我们的 Cookie 是在一个成功的返回中创建的,并没有被“ Header edit”或者“ Header always edit”所改变。显然阿帕奇有两桶饼干-见 这个

对我有用的是

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

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

然而,这种方法对我很有效,使用 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 Version 81.0.4044.129(正式版)(64位)
  • Microsoft 边缘版本81.0.416.68(正式版)(64位)
  • Firefox 75.0(64位)

如果汤姆猫使用弹簧靴,那么这已经在另一个问题中得到了回答。 总之,在 tom cat 配置上设置属性。这是全局的,所有 Cookie 将启用相同的站点。 (来自另一个问题 https://stackoverflow.com/a/60860531/400048)

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

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

了解有关解决方案的更多细节 JessessionId cookie 的 Samesite 只能从响应中设置

        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确实实现了这一点,并将其开源: 饼干头

使用方法:

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


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

如果你碰巧使用 Spring 框架,你可以利用 响应饼干类的优势。例如:

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

免责声明 : 标志及其值仅作为类的 API 的示例提供。