Add SameSite cookie support for servlet web servers
Update Tomcat, Jetty and Undertow `ServletWebServerFactory` implementations so that they can write SameSite cookie attributes. The session cookie will be customized whenever the `server.servlet.session.cookie.same-site` property is set. Other cookies can be customized with the new `CookieSameSiteSupplier` interface which can be registered using `@Bean` methods. Closes gh-20971 Co-authored-by Andy Wilkinson <wilkinsona@vmware.com>
This commit is contained in:
parent
efa04367b1
commit
cf9156e497
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
@ -41,6 +41,7 @@ import org.springframework.boot.web.server.ErrorPageRegistrarBeanPostProcessor;
|
|||
import org.springframework.boot.web.server.WebServerFactoryCustomizerBeanPostProcessor;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.boot.web.servlet.WebListenerRegistrar;
|
||||
import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
|
@ -73,9 +74,11 @@ public class ServletWebServerFactoryAutoConfiguration {
|
|||
|
||||
@Bean
|
||||
public ServletWebServerFactoryCustomizer servletWebServerFactoryCustomizer(ServerProperties serverProperties,
|
||||
ObjectProvider<WebListenerRegistrar> webListenerRegistrars) {
|
||||
ObjectProvider<WebListenerRegistrar> webListenerRegistrars,
|
||||
ObjectProvider<CookieSameSiteSupplier> cookieSameSiteSuppliers) {
|
||||
return new ServletWebServerFactoryCustomizer(serverProperties,
|
||||
webListenerRegistrars.orderedStream().collect(Collectors.toList()));
|
||||
webListenerRegistrars.orderedStream().collect(Collectors.toList()),
|
||||
cookieSameSiteSuppliers.orderedStream().collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
@ -24,11 +24,13 @@ import org.springframework.boot.context.properties.PropertyMapper;
|
|||
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
|
||||
import org.springframework.boot.web.servlet.WebListenerRegistrar;
|
||||
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
|
||||
import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* {@link WebServerFactoryCustomizer} to apply {@link ServerProperties} to servlet web
|
||||
* servers.
|
||||
* {@link WebServerFactoryCustomizer} to apply {@link ServerProperties} and
|
||||
* {@link WebListenerRegistrar WebListenerRegistrars} to servlet web servers.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Stephane Nicoll
|
||||
|
|
@ -41,7 +43,9 @@ public class ServletWebServerFactoryCustomizer
|
|||
|
||||
private final ServerProperties serverProperties;
|
||||
|
||||
private final Iterable<WebListenerRegistrar> webListenerRegistrars;
|
||||
private final List<WebListenerRegistrar> webListenerRegistrars;
|
||||
|
||||
private final List<CookieSameSiteSupplier> cookieSameSiteSuppliers;
|
||||
|
||||
public ServletWebServerFactoryCustomizer(ServerProperties serverProperties) {
|
||||
this(serverProperties, Collections.emptyList());
|
||||
|
|
@ -49,8 +53,14 @@ public class ServletWebServerFactoryCustomizer
|
|||
|
||||
public ServletWebServerFactoryCustomizer(ServerProperties serverProperties,
|
||||
List<WebListenerRegistrar> webListenerRegistrars) {
|
||||
this(serverProperties, webListenerRegistrars, null);
|
||||
}
|
||||
|
||||
ServletWebServerFactoryCustomizer(ServerProperties serverProperties,
|
||||
List<WebListenerRegistrar> webListenerRegistrars, List<CookieSameSiteSupplier> cookieSameSiteSuppliers) {
|
||||
this.serverProperties = serverProperties;
|
||||
this.webListenerRegistrars = webListenerRegistrars;
|
||||
this.cookieSameSiteSuppliers = cookieSameSiteSuppliers;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -77,6 +87,9 @@ public class ServletWebServerFactoryCustomizer
|
|||
for (WebListenerRegistrar registrar : this.webListenerRegistrars) {
|
||||
registrar.register(factory);
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(this.cookieSameSiteSuppliers)) {
|
||||
factory.setCookieSameSiteSuppliers(this.cookieSameSiteSuppliers);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,9 @@ import org.springframework.boot.web.server.WebServerFactoryCustomizer;
|
|||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.boot.web.servlet.ServletRegistrationBean;
|
||||
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
|
||||
import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory;
|
||||
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
|
||||
import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier;
|
||||
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
|
@ -369,6 +371,14 @@ class ServletWebServerFactoryAutoConfigurationTests {
|
|||
.run((context) -> assertThat(context).hasSingleBean(FilterRegistrationBean.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void cookieSameSiteSuppliersAreApplied() {
|
||||
this.contextRunner.withUserConfiguration(CookieSameSiteSupplierConfiguration.class).run((context) -> {
|
||||
AbstractServletWebServerFactory webServerFactory = context.getBean(AbstractServletWebServerFactory.class);
|
||||
assertThat(webServerFactory.getCookieSameSiteSuppliers()).hasSize(2);
|
||||
});
|
||||
}
|
||||
|
||||
private ContextConsumer<AssertableWebApplicationContext> verifyContext() {
|
||||
return this::verifyContext;
|
||||
}
|
||||
|
|
@ -656,4 +666,19 @@ class ServletWebServerFactoryAutoConfigurationTests {
|
|||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CookieSameSiteSupplierConfiguration {
|
||||
|
||||
@Bean
|
||||
CookieSameSiteSupplier cookieSameSiteSupplier1() {
|
||||
return CookieSameSiteSupplier.ofLax().whenHasName("test1");
|
||||
}
|
||||
|
||||
@Bean
|
||||
CookieSameSiteSupplier cookieSameSiteSupplier2() {
|
||||
return CookieSameSiteSupplier.ofNone().whenHasName("test2");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -588,6 +588,39 @@ TIP: See the {spring-boot-autoconfigure-module-code}/web/ServerProperties.java[`
|
|||
|
||||
|
||||
|
||||
[[web.servlet.embedded-container.customizing.samesite]]
|
||||
===== SameSite Cookies
|
||||
The `SameSite` cookie attribute can be used by web browsers to control if and how cookies are submitted in cross-site requests.
|
||||
The attribute is particularly relevant for modern web browsers which have started to change the default value that is used when the attribute is missing.
|
||||
|
||||
If you want to change the `SameSite` attribute of your session cookie, you can use the configprop:server.servlet.session.cookie.same-site[] property.
|
||||
This property is supported by auto-configured Tomcat, Jetty and Undertow servers.
|
||||
It is also used to configure Spring Session servlet based `SessionRepository` beans.
|
||||
|
||||
For example, if you want your session cookie to have a `SameSite` attribute of `None`, you can add the following to you `application.properties` or `application.yaml` file:
|
||||
|
||||
[source,yaml,indent=0,subs="verbatim",configprops,configblocks]
|
||||
----
|
||||
server:
|
||||
servlet:
|
||||
session:
|
||||
cookie:
|
||||
same-site: "none"
|
||||
----
|
||||
|
||||
If you want to change the `SameSite` attribute on other cookies added to your `HttpServletResponse`, you can use a `CookieSameSiteSupplier`.
|
||||
The `CookieSameSiteSupplier` is passed a `Cookie` and may return a `SameSite` value, or `null`.
|
||||
|
||||
There are a number of convenience factory and filter methods that you can use to quickly match specific cookies.
|
||||
For example, adding the following bean will automatically apply a `SameSite` of `Lax` for all cookies with a name that matches the regular expression `myapp.*`.
|
||||
|
||||
[source,java,indent=0,subs="verbatim"]
|
||||
----
|
||||
include::{docs-java}/web/servlet/embeddedcontainer/customizing/samesite/MySameSiteConfiguration.java[]
|
||||
----
|
||||
|
||||
|
||||
|
||||
[[web.servlet.embedded-container.customizing.programmatic]]
|
||||
===== Programmatic Customization
|
||||
If you need to programmatically configure your embedded servlet container, you can register a Spring bean that implements the `WebServerFactoryCustomizer` interface.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.docs.web.servlet.embeddedcontainer.customizing.samesite;
|
||||
|
||||
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.ofLax().whenHasNameMatching("myapp.*");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -33,6 +33,13 @@ import java.util.LinkedHashSet;
|
|||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpServletResponseWrapper;
|
||||
|
||||
import org.eclipse.jetty.http.HttpCookie;
|
||||
import org.eclipse.jetty.http.MimeTypes;
|
||||
import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory;
|
||||
import org.eclipse.jetty.server.AbstractConnector;
|
||||
|
|
@ -41,6 +48,7 @@ import org.eclipse.jetty.server.Connector;
|
|||
import org.eclipse.jetty.server.Handler;
|
||||
import org.eclipse.jetty.server.HttpConfiguration;
|
||||
import org.eclipse.jetty.server.HttpConnectionFactory;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.server.handler.ErrorHandler;
|
||||
|
|
@ -63,16 +71,19 @@ import org.eclipse.jetty.webapp.AbstractConfiguration;
|
|||
import org.eclipse.jetty.webapp.Configuration;
|
||||
import org.eclipse.jetty.webapp.WebAppContext;
|
||||
|
||||
import org.springframework.boot.web.server.Cookie.SameSite;
|
||||
import org.springframework.boot.web.server.ErrorPage;
|
||||
import org.springframework.boot.web.server.MimeMappings;
|
||||
import org.springframework.boot.web.server.Shutdown;
|
||||
import org.springframework.boot.web.server.WebServer;
|
||||
import org.springframework.boot.web.servlet.ServletContextInitializer;
|
||||
import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory;
|
||||
import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier;
|
||||
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
|
||||
import org.springframework.context.ResourceLoaderAware;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
|
|
@ -199,6 +210,9 @@ public class JettyServletWebServerFactory extends AbstractServletWebServerFactor
|
|||
if (StringUtils.hasText(getServerHeader())) {
|
||||
handler = applyWrapper(handler, JettyHandlerWrappers.createServerHeaderHandlerWrapper(getServerHeader()));
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(getCookieSameSiteSuppliers())) {
|
||||
handler = applyWrapper(handler, new SuppliedSameSiteCookieHandlerWrapper(getCookieSameSiteSuppliers()));
|
||||
}
|
||||
return handler;
|
||||
}
|
||||
|
||||
|
|
@ -245,6 +259,10 @@ public class JettyServletWebServerFactory extends AbstractServletWebServerFactor
|
|||
|
||||
private void configureSession(WebAppContext context) {
|
||||
SessionHandler handler = context.getSessionHandler();
|
||||
SameSite sessionSameSite = getSession().getCookie().getSameSite();
|
||||
if (sessionSameSite != null) {
|
||||
handler.setSameSite(HttpCookie.SameSite.valueOf(sessionSameSite.name()));
|
||||
}
|
||||
Duration sessionTimeout = getSession().getTimeout();
|
||||
handler.setMaxInactiveInterval(isNegative(sessionTimeout) ? -1 : (int) sessionTimeout.getSeconds());
|
||||
if (getSession().isPersistent()) {
|
||||
|
|
@ -661,4 +679,66 @@ public class JettyServletWebServerFactory extends AbstractServletWebServerFactor
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link HandlerWrapper} to apply {@link CookieSameSiteSupplier supplied}
|
||||
* {@link SameSite} cookie values.
|
||||
*/
|
||||
private static class SuppliedSameSiteCookieHandlerWrapper extends HandlerWrapper {
|
||||
|
||||
private final List<CookieSameSiteSupplier> suppliers;
|
||||
|
||||
SuppliedSameSiteCookieHandlerWrapper(List<CookieSameSiteSupplier> suppliers) {
|
||||
this.suppliers = suppliers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
|
||||
throws IOException, ServletException {
|
||||
HttpServletResponse wrappedResponse = new ResposeWrapper(response);
|
||||
super.handle(target, baseRequest, request, wrappedResponse);
|
||||
}
|
||||
|
||||
class ResposeWrapper extends HttpServletResponseWrapper {
|
||||
|
||||
ResposeWrapper(HttpServletResponse response) {
|
||||
super(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addCookie(Cookie cookie) {
|
||||
SameSite sameSite = getSameSite(cookie);
|
||||
if (sameSite != null) {
|
||||
String comment = HttpCookie.getCommentWithoutAttributes(cookie.getComment());
|
||||
String sameSiteComment = getSameSiteComment(sameSite);
|
||||
cookie.setComment((comment != null) ? comment + sameSiteComment : sameSiteComment);
|
||||
}
|
||||
super.addCookie(cookie);
|
||||
}
|
||||
|
||||
private String getSameSiteComment(SameSite sameSite) {
|
||||
switch (sameSite) {
|
||||
case NONE:
|
||||
return HttpCookie.SAME_SITE_NONE_COMMENT;
|
||||
case LAX:
|
||||
return HttpCookie.SAME_SITE_LAX_COMMENT;
|
||||
case STRICT:
|
||||
return HttpCookie.SAME_SITE_STRICT_COMMENT;
|
||||
}
|
||||
throw new IllegalStateException("Unsupported SameSite value " + sameSite);
|
||||
}
|
||||
|
||||
private SameSite getSameSite(Cookie cookie) {
|
||||
for (CookieSameSiteSupplier supplier : SuppliedSameSiteCookieHandlerWrapper.this.suppliers) {
|
||||
SameSite sameSite = supplier.getSameSite(cookie);
|
||||
if (sameSite != null) {
|
||||
return sameSite;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ import java.util.Set;
|
|||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.servlet.ServletContainerInitializer;
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.apache.catalina.Context;
|
||||
import org.apache.catalina.Engine;
|
||||
|
|
@ -55,26 +57,31 @@ import org.apache.catalina.session.StandardManager;
|
|||
import org.apache.catalina.startup.Tomcat;
|
||||
import org.apache.catalina.startup.Tomcat.FixContextListener;
|
||||
import org.apache.catalina.util.LifecycleBase;
|
||||
import org.apache.catalina.util.SessionConfig;
|
||||
import org.apache.catalina.webresources.AbstractResourceSet;
|
||||
import org.apache.catalina.webresources.EmptyResource;
|
||||
import org.apache.catalina.webresources.StandardRoot;
|
||||
import org.apache.coyote.AbstractProtocol;
|
||||
import org.apache.coyote.ProtocolHandler;
|
||||
import org.apache.coyote.http2.Http2Protocol;
|
||||
import org.apache.tomcat.util.http.Rfc6265CookieProcessor;
|
||||
import org.apache.tomcat.util.modeler.Registry;
|
||||
import org.apache.tomcat.util.scan.StandardJarScanFilter;
|
||||
|
||||
import org.springframework.boot.util.LambdaSafe;
|
||||
import org.springframework.boot.web.server.Cookie.SameSite;
|
||||
import org.springframework.boot.web.server.ErrorPage;
|
||||
import org.springframework.boot.web.server.MimeMappings;
|
||||
import org.springframework.boot.web.server.WebServer;
|
||||
import org.springframework.boot.web.servlet.ServletContextInitializer;
|
||||
import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory;
|
||||
import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier;
|
||||
import org.springframework.context.ResourceLoaderAware;
|
||||
import org.springframework.core.NativeDetector;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
|
|
@ -381,6 +388,7 @@ public class TomcatServletWebServerFactory extends AbstractServletWebServerFacto
|
|||
context.addMimeMapping(mapping.getExtension(), mapping.getMimeType());
|
||||
}
|
||||
configureSession(context);
|
||||
configureCookieProcessor(context);
|
||||
new DisableReferenceClearingContextCustomizer().customize(context);
|
||||
for (String webListenerClassName : getWebListenerClassNames()) {
|
||||
context.addApplicationListener(webListenerClassName);
|
||||
|
|
@ -410,6 +418,21 @@ public class TomcatServletWebServerFactory extends AbstractServletWebServerFacto
|
|||
}
|
||||
}
|
||||
|
||||
private void configureCookieProcessor(Context context) {
|
||||
SameSite sessionSameSite = getSession().getCookie().getSameSite();
|
||||
List<CookieSameSiteSupplier> suppliers = new ArrayList<>();
|
||||
if (sessionSameSite != null) {
|
||||
suppliers.add(CookieSameSiteSupplier.of(sessionSameSite)
|
||||
.whenHasName(() -> SessionConfig.getSessionCookieName(context)));
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(getCookieSameSiteSuppliers())) {
|
||||
suppliers.addAll(getCookieSameSiteSuppliers());
|
||||
}
|
||||
if (!suppliers.isEmpty()) {
|
||||
context.setCookieProcessor(new SuppliedSameSiteCookieProcessor(suppliers));
|
||||
}
|
||||
}
|
||||
|
||||
private void configurePersistSession(Manager manager) {
|
||||
Assert.state(manager instanceof StandardManager,
|
||||
() -> "Unable to persist HTTP session state using manager type " + manager.getClass().getName());
|
||||
|
|
@ -880,4 +903,39 @@ public class TomcatServletWebServerFactory extends AbstractServletWebServerFacto
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Rfc6265CookieProcessor} that supports {@link CookieSameSiteSupplier
|
||||
* supplied} {@link SameSite} values.
|
||||
*/
|
||||
private static class SuppliedSameSiteCookieProcessor extends Rfc6265CookieProcessor {
|
||||
|
||||
private final List<CookieSameSiteSupplier> suppliers;
|
||||
|
||||
SuppliedSameSiteCookieProcessor(List<CookieSameSiteSupplier> suppliers) {
|
||||
this.suppliers = suppliers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateHeader(Cookie cookie, HttpServletRequest request) {
|
||||
SameSite sameSite = getSameSite(cookie);
|
||||
if (sameSite == null) {
|
||||
return super.generateHeader(cookie, request);
|
||||
}
|
||||
Rfc6265CookieProcessor delegate = new Rfc6265CookieProcessor();
|
||||
delegate.setSameSiteCookies(sameSite.attributeValue());
|
||||
return delegate.generateHeader(cookie, request);
|
||||
}
|
||||
|
||||
private SameSite getSameSite(Cookie cookie) {
|
||||
for (CookieSameSiteSupplier supplier : this.suppliers) {
|
||||
SameSite sameSite = supplier.getSameSite(cookie);
|
||||
if (sameSite != null) {
|
||||
return sameSite;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,9 @@ import javax.servlet.ServletContext;
|
|||
import javax.servlet.ServletException;
|
||||
|
||||
import io.undertow.Undertow.Builder;
|
||||
import io.undertow.server.HttpHandler;
|
||||
import io.undertow.server.HttpServerExchange;
|
||||
import io.undertow.server.handlers.Cookie;
|
||||
import io.undertow.server.handlers.resource.FileResourceManager;
|
||||
import io.undertow.server.handlers.resource.Resource;
|
||||
import io.undertow.server.handlers.resource.ResourceChangeListener;
|
||||
|
|
@ -46,6 +49,7 @@ import io.undertow.server.handlers.resource.ResourceManager;
|
|||
import io.undertow.server.handlers.resource.URLResource;
|
||||
import io.undertow.server.session.SessionManager;
|
||||
import io.undertow.servlet.Servlets;
|
||||
import io.undertow.servlet.api.Deployment;
|
||||
import io.undertow.servlet.api.DeploymentInfo;
|
||||
import io.undertow.servlet.api.DeploymentManager;
|
||||
import io.undertow.servlet.api.ListenerInfo;
|
||||
|
|
@ -56,15 +60,19 @@ import io.undertow.servlet.core.DeploymentImpl;
|
|||
import io.undertow.servlet.handlers.DefaultServlet;
|
||||
import io.undertow.servlet.util.ImmediateInstanceFactory;
|
||||
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.web.server.Cookie.SameSite;
|
||||
import org.springframework.boot.web.server.ErrorPage;
|
||||
import org.springframework.boot.web.server.MimeMappings.Mapping;
|
||||
import org.springframework.boot.web.server.WebServer;
|
||||
import org.springframework.boot.web.servlet.ServletContextInitializer;
|
||||
import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory;
|
||||
import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier;
|
||||
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
|
||||
import org.springframework.context.ResourceLoaderAware;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* {@link ServletWebServerFactory} that can be used to create
|
||||
|
|
@ -415,9 +423,9 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac
|
|||
}
|
||||
}
|
||||
|
||||
private void configureErrorPages(DeploymentInfo servletBuilder) {
|
||||
private void configureErrorPages(DeploymentInfo deployment) {
|
||||
for (ErrorPage errorPage : getErrorPages()) {
|
||||
servletBuilder.addErrorPage(getUndertowErrorPage(errorPage));
|
||||
deployment.addErrorPage(getUndertowErrorPage(errorPage));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -431,9 +439,9 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac
|
|||
return new io.undertow.servlet.api.ErrorPage(errorPage.getPath());
|
||||
}
|
||||
|
||||
private void configureMimeMappings(DeploymentInfo servletBuilder) {
|
||||
private void configureMimeMappings(DeploymentInfo deployment) {
|
||||
for (Mapping mimeMapping : getMimeMappings()) {
|
||||
servletBuilder.addMimeMapping(new MimeMapping(mimeMapping.getExtension(), mimeMapping.getMimeType()));
|
||||
deployment.addMimeMapping(new MimeMapping(mimeMapping.getExtension(), mimeMapping.getMimeType()));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -458,11 +466,30 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac
|
|||
* @return a new {@link UndertowServletWebServer} instance
|
||||
*/
|
||||
protected UndertowServletWebServer getUndertowWebServer(Builder builder, DeploymentManager manager, int port) {
|
||||
List<HttpHandlerFactory> initialHandlerFactories = new ArrayList<>();
|
||||
initialHandlerFactories.add(new DeploymentManagerHttpHandlerFactory(manager));
|
||||
HttpHandlerFactory cooHandlerFactory = getCookieHandlerFactory(manager.getDeployment());
|
||||
if (cooHandlerFactory != null) {
|
||||
initialHandlerFactories.add(cooHandlerFactory);
|
||||
}
|
||||
List<HttpHandlerFactory> httpHandlerFactories = this.delegate.createHttpHandlerFactories(this,
|
||||
new DeploymentManagerHttpHandlerFactory(manager));
|
||||
initialHandlerFactories.toArray(new HttpHandlerFactory[0]));
|
||||
return new UndertowServletWebServer(builder, httpHandlerFactories, getContextPath(), port >= 0);
|
||||
}
|
||||
|
||||
private HttpHandlerFactory getCookieHandlerFactory(Deployment deployment) {
|
||||
SameSite sessionSameSite = getSession().getCookie().getSameSite();
|
||||
List<CookieSameSiteSupplier> suppliers = new ArrayList<>();
|
||||
if (sessionSameSite != null) {
|
||||
String sessionCookieName = deployment.getServletContext().getSessionCookieConfig().getName();
|
||||
suppliers.add(CookieSameSiteSupplier.of(sessionSameSite).whenHasName(sessionCookieName));
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(getCookieSameSiteSuppliers())) {
|
||||
suppliers.addAll(getCookieSameSiteSuppliers());
|
||||
}
|
||||
return (!suppliers.isEmpty()) ? (next) -> new SuppliedSameSiteCookieHandler(next, suppliers) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ServletContainerInitializer} to initialize {@link ServletContextInitializer
|
||||
* ServletContextInitializers}.
|
||||
|
|
@ -583,4 +610,59 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link HttpHandler} to apply {@link CookieSameSiteSupplier supplied}
|
||||
* {@link SameSite} cookie values.
|
||||
*/
|
||||
private static class SuppliedSameSiteCookieHandler implements HttpHandler {
|
||||
|
||||
private final HttpHandler next;
|
||||
|
||||
private final List<CookieSameSiteSupplier> suppliers;
|
||||
|
||||
SuppliedSameSiteCookieHandler(HttpHandler next, List<CookieSameSiteSupplier> suppliers) {
|
||||
this.next = next;
|
||||
this.suppliers = suppliers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRequest(HttpServerExchange exchange) throws Exception {
|
||||
exchange.addResponseCommitListener(this::beforeCommit);
|
||||
this.next.handleRequest(exchange);
|
||||
}
|
||||
|
||||
private void beforeCommit(HttpServerExchange exchange) {
|
||||
for (Cookie cookie : exchange.responseCookies()) {
|
||||
SameSite sameSite = getSameSite(asServletCookie(cookie));
|
||||
if (sameSite != null) {
|
||||
cookie.setSameSiteMode(sameSite.attributeValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private javax.servlet.http.Cookie asServletCookie(Cookie cookie) {
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
javax.servlet.http.Cookie result = new javax.servlet.http.Cookie(cookie.getName(), cookie.getValue());
|
||||
map.from(cookie::getComment).to(result::setComment);
|
||||
map.from(cookie::getDomain).to(result::setDomain);
|
||||
map.from(cookie::getMaxAge).to(result::setMaxAge);
|
||||
map.from(cookie::getPath).to(result::setPath);
|
||||
result.setSecure(cookie.isSecure());
|
||||
result.setVersion(cookie.getVersion());
|
||||
result.setHttpOnly(cookie.isHttpOnly());
|
||||
return result;
|
||||
}
|
||||
|
||||
private SameSite getSameSite(javax.servlet.http.Cookie cookie) {
|
||||
for (CookieSameSiteSupplier supplier : this.suppliers) {
|
||||
SameSite sameSite = supplier.getSameSite(cookie);
|
||||
if (sameSite != null) {
|
||||
return sameSite;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,6 +80,8 @@ public abstract class AbstractServletWebServerFactory extends AbstractConfigurab
|
|||
|
||||
private Map<String, String> initParameters = Collections.emptyMap();
|
||||
|
||||
private List<CookieSameSiteSupplier> cookieSameSiteSuppliers = new ArrayList<>();
|
||||
|
||||
private final DocumentRoot documentRoot = new DocumentRoot(this.logger);
|
||||
|
||||
private final StaticResourceJars staticResourceJars = new StaticResourceJars();
|
||||
|
|
@ -242,6 +244,22 @@ public abstract class AbstractServletWebServerFactory extends AbstractConfigurab
|
|||
return this.initParameters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCookieSameSiteSuppliers(List<? extends CookieSameSiteSupplier> cookieSameSiteSuppliers) {
|
||||
Assert.notNull(cookieSameSiteSuppliers, "CookieSameSiteSuppliers must not be null");
|
||||
this.cookieSameSiteSuppliers = new ArrayList<>(cookieSameSiteSuppliers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addCookieSameSiteSuppliers(CookieSameSiteSupplier... cookieSameSiteSuppliers) {
|
||||
Assert.notNull(cookieSameSiteSuppliers, "CookieSameSiteSuppliers must not be null");
|
||||
this.cookieSameSiteSuppliers.addAll(Arrays.asList(cookieSameSiteSuppliers));
|
||||
}
|
||||
|
||||
public List<CookieSameSiteSupplier> getCookieSameSiteSuppliers() {
|
||||
return this.cookieSameSiteSuppliers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method that can be used by subclasses wishing to combine the specified
|
||||
* {@link ServletContextInitializer} parameters with those defined in this instance.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
@ -25,6 +25,7 @@ import java.util.Map;
|
|||
import javax.servlet.ServletContext;
|
||||
|
||||
import org.springframework.boot.web.server.ConfigurableWebServerFactory;
|
||||
import org.springframework.boot.web.server.Cookie.SameSite;
|
||||
import org.springframework.boot.web.server.MimeMappings;
|
||||
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
|
||||
import org.springframework.boot.web.servlet.ServletContextInitializer;
|
||||
|
|
@ -125,4 +126,21 @@ public interface ConfigurableServletWebServerFactory
|
|||
*/
|
||||
void setInitParameters(Map<String, String> initParameters);
|
||||
|
||||
/**
|
||||
* Sets {@link CookieSameSiteSupplier CookieSameSiteSuppliers} that should be used to
|
||||
* obtain the {@link SameSite} attribute of any added cookie. This method will replace
|
||||
* any previously set or added suppliers.
|
||||
* @param cookieSameSiteSuppliers the suppliers to add
|
||||
* @see #addCookieSameSiteSuppliers
|
||||
*/
|
||||
void setCookieSameSiteSuppliers(List<? extends CookieSameSiteSupplier> cookieSameSiteSuppliers);
|
||||
|
||||
/**
|
||||
* Add {@link CookieSameSiteSupplier CookieSameSiteSuppliers} to those that should be
|
||||
* used to obtain the {@link SameSite} attribute of any added cookie.
|
||||
* @param cookieSameSiteSuppliers the suppliers to add
|
||||
* @see #setCookieSameSiteSuppliers
|
||||
*/
|
||||
void addCookieSameSiteSuppliers(CookieSameSiteSupplier... cookieSameSiteSuppliers);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.servlet.server;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
import javax.servlet.http.Cookie;
|
||||
|
||||
import org.springframework.boot.web.server.Cookie.SameSite;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Strategy interface that can be used with {@link ConfigurableServletWebServerFactory}
|
||||
* implementations in order to supply custom {@link SameSite} values for specific
|
||||
* {@link Cookie cookies}.
|
||||
* <p>
|
||||
* Basic CookieSameSiteSupplier implementations can be constructed using the {@code of...}
|
||||
* factory methods, typically combined with name matching. For example: <pre class="code">
|
||||
* CookieSameSiteSupplier.ofLax().whenHasName("mycookie");
|
||||
* </pre>
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.6.0
|
||||
* @see ConfigurableServletWebServerFactory#addCookieSameSiteSuppliers(CookieSameSiteSupplier...)
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface CookieSameSiteSupplier {
|
||||
|
||||
/**
|
||||
* Get the {@link SameSite} values that should be used for the given {@link Cookie}.
|
||||
* @param cookie the cookie to check
|
||||
* @return the {@link SameSite} value to use or {@code null} if the next supplier
|
||||
* should be checked
|
||||
*/
|
||||
SameSite getSameSite(Cookie cookie);
|
||||
|
||||
/**
|
||||
* Limit this supplier so that it's only called if the Cookie has the given name.
|
||||
* @param name the name to check
|
||||
* @return a new {@link CookieSameSiteSupplier} that only calls this supplier when the
|
||||
* name matches
|
||||
*/
|
||||
default CookieSameSiteSupplier whenHasName(String name) {
|
||||
Assert.hasText(name, "Name must not be empty");
|
||||
return when((cookie) -> ObjectUtils.nullSafeEquals(cookie.getName(), name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit this supplier so that it's only called if the Cookie has the given name.
|
||||
* @param nameSupplier a supplier providing the name to check
|
||||
* @return a new {@link CookieSameSiteSupplier} that only calls this supplier when the
|
||||
* name matches
|
||||
*/
|
||||
default CookieSameSiteSupplier whenHasName(Supplier<String> nameSupplier) {
|
||||
Assert.notNull(nameSupplier, "NameSupplier must not be empty");
|
||||
return when((cookie) -> ObjectUtils.nullSafeEquals(cookie.getName(), nameSupplier.get()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit this supplier so that it's only called if the Cookie name matches the given
|
||||
* regex.
|
||||
* @param regex the regex pattern that must match
|
||||
* @return a new {@link CookieSameSiteSupplier} that only calls this supplier when the
|
||||
* name matches the regex
|
||||
*/
|
||||
default CookieSameSiteSupplier whenHasNameMatching(String regex) {
|
||||
Assert.hasText(regex, "Regex must not be empty");
|
||||
return whenHasNameMatching(Pattern.compile(regex));
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit this supplier so that it's only called if the Cookie name matches the given
|
||||
* {@link Pattern}.
|
||||
* @param pattern the regex pattern that must match
|
||||
* @return a new {@link CookieSameSiteSupplier} that only calls this supplier when the
|
||||
* name matches the pattern
|
||||
*/
|
||||
default CookieSameSiteSupplier whenHasNameMatching(Pattern pattern) {
|
||||
Assert.notNull(pattern, "Pattern must not be null");
|
||||
return when((cookie) -> pattern.matcher(cookie.getName()).matches());
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit this supplier so that it's only called if the predicate accepts the Cookie.
|
||||
* @param predicate the predicate used to match the cookie
|
||||
* @return a new {@link CookieSameSiteSupplier} that only calls this supplier when the
|
||||
* cookie matches the predicate
|
||||
*/
|
||||
default CookieSameSiteSupplier when(Predicate<Cookie> predicate) {
|
||||
Assert.notNull(predicate, "Predicate must not be null");
|
||||
return (cookie) -> predicate.test(cookie) ? getSameSite(cookie) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link CookieSameSiteSupplier} that always returns
|
||||
* {@link SameSite#NONE}.
|
||||
* @return the supplier instance
|
||||
*/
|
||||
static CookieSameSiteSupplier ofNone() {
|
||||
return of(SameSite.NONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link CookieSameSiteSupplier} that always returns
|
||||
* {@link SameSite#LAX}.
|
||||
* @return the supplier instance
|
||||
*/
|
||||
static CookieSameSiteSupplier ofLax() {
|
||||
return of(SameSite.LAX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link CookieSameSiteSupplier} that always returns
|
||||
* {@link SameSite#STRICT}.
|
||||
* @return the supplier instance
|
||||
*/
|
||||
static CookieSameSiteSupplier ofStrict() {
|
||||
return of(SameSite.STRICT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link CookieSameSiteSupplier} that always returns the given
|
||||
* {@link SameSite} value.
|
||||
* @param sameSite the value to return
|
||||
* @return the supplier instance
|
||||
*/
|
||||
static CookieSameSiteSupplier of(SameSite sameSite) {
|
||||
Assert.notNull(sameSite, "SameSite must not be null");
|
||||
return (cookie) -> sameSite;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -42,6 +42,7 @@ import java.util.Collections;
|
|||
import java.util.Date;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
|
@ -73,6 +74,7 @@ import javax.servlet.ServletRegistration.Dynamic;
|
|||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.SessionCookieConfig;
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
|
@ -105,6 +107,8 @@ import org.junit.jupiter.api.Assumptions;
|
|||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.EnumSource;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import org.springframework.boot.system.ApplicationHome;
|
||||
|
|
@ -114,6 +118,7 @@ import org.springframework.boot.testsupport.system.OutputCaptureExtension;
|
|||
import org.springframework.boot.testsupport.web.servlet.ExampleFilter;
|
||||
import org.springframework.boot.testsupport.web.servlet.ExampleServlet;
|
||||
import org.springframework.boot.web.server.Compression;
|
||||
import org.springframework.boot.web.server.Cookie.SameSite;
|
||||
import org.springframework.boot.web.server.ErrorPage;
|
||||
import org.springframework.boot.web.server.GracefulShutdownResult;
|
||||
import org.springframework.boot.web.server.Http2;
|
||||
|
|
@ -810,6 +815,57 @@ public abstract class AbstractServletWebServerFactoryTests {
|
|||
assertThat(sessionCookieConfig.getMaxAge()).isEqualTo(60);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(SameSite.class)
|
||||
void sessionCookieSameSiteAttributeCanBeConfiguredAndOnlyAffectsSessionCookies(SameSite sameSite) throws Exception {
|
||||
AbstractServletWebServerFactory factory = getFactory();
|
||||
factory.getSession().getCookie().setSameSite(sameSite);
|
||||
factory.addInitializers(new ServletRegistrationBean<>(new CookieServlet(false), "/"));
|
||||
this.webServer = factory.getWebServer();
|
||||
this.webServer.start();
|
||||
ClientHttpResponse clientResponse = getClientResponse(getLocalUrl("/"));
|
||||
List<String> setCookieHeaders = clientResponse.getHeaders().get("Set-Cookie");
|
||||
assertThat(setCookieHeaders).satisfiesExactlyInAnyOrder(
|
||||
(header) -> assertThat(header).contains("JSESSIONID").contains("SameSite=" + sameSite.attributeValue()),
|
||||
(header) -> assertThat(header).contains("test=test").doesNotContain("SameSite"));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(SameSite.class)
|
||||
void sessionCookieSameSiteAttributeCanBeConfiguredAndOnlyAffectsSessionCookiesWhenUsingCustomName(SameSite sameSite)
|
||||
throws Exception {
|
||||
AbstractServletWebServerFactory factory = getFactory();
|
||||
factory.getSession().getCookie().setName("THESESSION");
|
||||
factory.getSession().getCookie().setSameSite(sameSite);
|
||||
factory.addInitializers(new ServletRegistrationBean<>(new CookieServlet(false), "/"));
|
||||
this.webServer = factory.getWebServer();
|
||||
this.webServer.start();
|
||||
ClientHttpResponse clientResponse = getClientResponse(getLocalUrl("/"));
|
||||
List<String> setCookieHeaders = clientResponse.getHeaders().get("Set-Cookie");
|
||||
assertThat(setCookieHeaders).satisfiesExactlyInAnyOrder(
|
||||
(header) -> assertThat(header).contains("THESESSION").contains("SameSite=" + sameSite.attributeValue()),
|
||||
(header) -> assertThat(header).contains("test=test").doesNotContain("SameSite"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void cookieSameSiteSuppliers() throws Exception {
|
||||
AbstractServletWebServerFactory factory = getFactory();
|
||||
factory.addCookieSameSiteSuppliers(CookieSameSiteSupplier.ofLax().whenHasName("relaxed"));
|
||||
factory.addCookieSameSiteSuppliers(CookieSameSiteSupplier.ofNone().whenHasName("empty"));
|
||||
factory.addCookieSameSiteSuppliers(CookieSameSiteSupplier.ofStrict().whenHasName("controlled"));
|
||||
factory.addInitializers(new ServletRegistrationBean<>(new CookieServlet(true), "/"));
|
||||
this.webServer = factory.getWebServer();
|
||||
this.webServer.start();
|
||||
ClientHttpResponse clientResponse = getClientResponse(getLocalUrl("/"));
|
||||
List<String> setCookieHeaders = clientResponse.getHeaders().get("Set-Cookie");
|
||||
assertThat(setCookieHeaders).satisfiesExactlyInAnyOrder(
|
||||
(header) -> assertThat(header).contains("JSESSIONID").doesNotContain("SameSite"),
|
||||
(header) -> assertThat(header).contains("test=test").doesNotContain("SameSite"),
|
||||
(header) -> assertThat(header).contains("relaxed=test").contains("SameSite=Lax"),
|
||||
(header) -> assertThat(header).contains("empty=test").contains("SameSite=None"),
|
||||
(header) -> assertThat(header).contains("controlled=test").contains("SameSite=Strict"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sslSessionTracking() {
|
||||
AbstractServletWebServerFactory factory = getFactory();
|
||||
|
|
@ -1611,4 +1667,25 @@ public abstract class AbstractServletWebServerFactoryTests {
|
|||
|
||||
}
|
||||
|
||||
static final class CookieServlet extends HttpServlet {
|
||||
|
||||
private final boolean addSupplierTestCookies;
|
||||
|
||||
CookieServlet(boolean addSupplierTestCookies) {
|
||||
this.addSupplierTestCookies = addSupplierTestCookies;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
|
||||
req.getSession(true);
|
||||
resp.addCookie(new Cookie("test", "test"));
|
||||
if (this.addSupplierTestCookies) {
|
||||
resp.addCookie(new Cookie("relaxed", "test"));
|
||||
resp.addCookie(new Cookie("empty", "test"));
|
||||
resp.addCookie(new Cookie("controlled", "test"));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.servlet.server;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.servlet.http.Cookie;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.web.server.Cookie.SameSite;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
|
||||
/**
|
||||
* Tests for {@link CookieSameSiteSupplier}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class CookieSameSiteSupplierTests {
|
||||
|
||||
@Test
|
||||
void whenHasNameWhenNameIsNullThrowsException() {
|
||||
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> supplier.whenHasName((String) null))
|
||||
.withMessage("Name must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasNameWhenNameIsEmptyThrowsException() {
|
||||
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> supplier.whenHasName(""))
|
||||
.withMessage("Name must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasNameWhenNameMatchesCallsGetSameSite() {
|
||||
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
|
||||
assertThat(supplier.whenHasName("test").getSameSite(new Cookie("test", "x"))).isEqualTo(SameSite.LAX);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasNameWhenNameDoesNotMatchDoesNotCallGetSameSite() {
|
||||
CookieSameSiteSupplier supplier = (cookie) -> fail("Supplier Called");
|
||||
assertThat(supplier.whenHasName("test").getSameSite(new Cookie("tset", "x"))).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasSuppliedNameWhenNameIsNullThrowsException() {
|
||||
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> supplier.whenHasName((Supplier<String>) null))
|
||||
.withMessage("NameSupplier must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasSuppliedNameWhenNameMatchesCallsGetSameSite() {
|
||||
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
|
||||
assertThat(supplier.whenHasName(() -> "test").getSameSite(new Cookie("test", "x"))).isEqualTo(SameSite.LAX);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasSuppliedNameWhenNameDoesNotMatchDoesNotCallGetSameSite() {
|
||||
CookieSameSiteSupplier supplier = (cookie) -> fail("Supplier Called");
|
||||
assertThat(supplier.whenHasName(() -> "test").getSameSite(new Cookie("tset", "x"))).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasNameMatchingRegexWhenRegexIsNullThrowsException() {
|
||||
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> supplier.whenHasNameMatching((String) null))
|
||||
.withMessage("Regex must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasNameMatchingRegexWhenRegexIsEmptyThrowsException() {
|
||||
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> supplier.whenHasNameMatching(""))
|
||||
.withMessage("Regex must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasNameMatchingRegexWhenNameMatchesCallsGetSameSite() {
|
||||
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
|
||||
assertThat(supplier.whenHasNameMatching("te.*").getSameSite(new Cookie("test", "x"))).isEqualTo(SameSite.LAX);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasNameMatchingRegexWhenNameDoesNotMatchDoesNotCallGetSameSite() {
|
||||
CookieSameSiteSupplier supplier = (cookie) -> fail("Supplier Called");
|
||||
assertThat(supplier.whenHasNameMatching("te.*").getSameSite(new Cookie("tset", "x"))).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasNameMatchingPatternWhenPatternIsNullThrowsException() {
|
||||
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> supplier.whenHasNameMatching((Pattern) null))
|
||||
.withMessage("Pattern must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasNameMatchingPatternWhenNameMatchesCallsGetSameSite() {
|
||||
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
|
||||
assertThat(supplier.whenHasNameMatching(Pattern.compile("te.*")).getSameSite(new Cookie("test", "x")))
|
||||
.isEqualTo(SameSite.LAX);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasNameMatchingPatternWhenNameDoesNotMatchDoesNotCallGetSameSite() {
|
||||
CookieSameSiteSupplier supplier = (cookie) -> fail("Supplier Called");
|
||||
assertThat(supplier.whenHasNameMatching(Pattern.compile("te.*")).getSameSite(new Cookie("tset", "x"))).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenWhenPredicateIsNullThrowsException() {
|
||||
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> supplier.when(null))
|
||||
.withMessage("Predicate must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenWhenPredicateMatchesCallsGetSameSite() {
|
||||
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
|
||||
assertThat(supplier.when((cookie) -> cookie.getName().equals("test")).getSameSite(new Cookie("test", "x")))
|
||||
.isEqualTo(SameSite.LAX);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenWhenPredicateDoesNotMatchDoesNotCallGetSameSite() {
|
||||
CookieSameSiteSupplier supplier = (cookie) -> fail("Supplier Called");
|
||||
assertThat(supplier.when((cookie) -> cookie.getName().equals("test")).getSameSite(new Cookie("tset", "x")))
|
||||
.isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofNoneSuppliesNone() {
|
||||
assertThat(CookieSameSiteSupplier.ofNone().getSameSite(new Cookie("test", "x"))).isEqualTo(SameSite.NONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofLaxSuppliesLax() {
|
||||
assertThat(CookieSameSiteSupplier.ofLax().getSameSite(new Cookie("test", "x"))).isEqualTo(SameSite.LAX);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofStrictSuppliesStrict() {
|
||||
assertThat(CookieSameSiteSupplier.ofStrict().getSameSite(new Cookie("test", "x"))).isEqualTo(SameSite.STRICT);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWhenNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> CookieSameSiteSupplier.of(null))
|
||||
.withMessage("SameSite must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofSuppliesValue() {
|
||||
assertThat(CookieSameSiteSupplier.of(SameSite.STRICT).getSameSite(new Cookie("test", "x")))
|
||||
.isEqualTo(SameSite.STRICT);
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue