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:
Phillip Webb
2021-10-20 22:13:06 -07:00
parent efa04367b1
commit cf9156e497
13 changed files with 781 additions and 13 deletions

View File

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

View File

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

View File

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

View File

@@ -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.

View File

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

View File

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

View File

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

View File

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