如何在运行于WildFly上的Web应用程序中启用SameSite?查看了standalone.xml
,但是没有找到合适的标签。
<servlet-container name="default">
<session-cookie http-only="true" secure="true"/>
<jsp-config/>
</servlet-container>
如何在运行于WildFly上的Web应用程序中启用SameSite?查看了standalone.xml
,但是没有找到合适的标签。
<servlet-container name="default">
<session-cookie http-only="true" secure="true"/>
<jsp-config/>
</servlet-container>
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不支持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属性。
一种解决方法是通过使用另一个属性(例如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_VALIDATION
为true
:
./bin/standalone.sh -Dio.undertow.cookie.DEFAULT_ENABLE_RFC6265_COOKIE_VALIDATION=true
对于当前最新版本的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);
}
});
}
}
如果您使用的是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>
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。我的解决方法,在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>
standalone.conf
中配置expression-filter
来实现,如this answer所述。 - Martin Höller