Skip to content

Commit cf9156e

Browse files
committed
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 <[email protected]>
1 parent efa0436 commit cf9156e

File tree

13 files changed

+781
-13
lines changed

13 files changed

+781
-13
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfiguration.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -41,6 +41,7 @@
4141
import org.springframework.boot.web.server.WebServerFactoryCustomizerBeanPostProcessor;
4242
import org.springframework.boot.web.servlet.FilterRegistrationBean;
4343
import org.springframework.boot.web.servlet.WebListenerRegistrar;
44+
import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier;
4445
import org.springframework.context.annotation.Bean;
4546
import org.springframework.context.annotation.Configuration;
4647
import org.springframework.context.annotation.Import;
@@ -73,9 +74,11 @@ public class ServletWebServerFactoryAutoConfiguration {
7374

7475
@Bean
7576
public ServletWebServerFactoryCustomizer servletWebServerFactoryCustomizer(ServerProperties serverProperties,
76-
ObjectProvider<WebListenerRegistrar> webListenerRegistrars) {
77+
ObjectProvider<WebListenerRegistrar> webListenerRegistrars,
78+
ObjectProvider<CookieSameSiteSupplier> cookieSameSiteSuppliers) {
7779
return new ServletWebServerFactoryCustomizer(serverProperties,
78-
webListenerRegistrars.orderedStream().collect(Collectors.toList()));
80+
webListenerRegistrars.orderedStream().collect(Collectors.toList()),
81+
cookieSameSiteSuppliers.orderedStream().collect(Collectors.toList()));
7982
}
8083

8184
@Bean

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizer.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -24,11 +24,13 @@
2424
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
2525
import org.springframework.boot.web.servlet.WebListenerRegistrar;
2626
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
27+
import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier;
2728
import org.springframework.core.Ordered;
29+
import org.springframework.util.CollectionUtils;
2830

2931
/**
30-
* {@link WebServerFactoryCustomizer} to apply {@link ServerProperties} to servlet web
31-
* servers.
32+
* {@link WebServerFactoryCustomizer} to apply {@link ServerProperties} and
33+
* {@link WebListenerRegistrar WebListenerRegistrars} to servlet web servers.
3234
*
3335
* @author Brian Clozel
3436
* @author Stephane Nicoll
@@ -41,16 +43,24 @@ public class ServletWebServerFactoryCustomizer
4143

4244
private final ServerProperties serverProperties;
4345

44-
private final Iterable<WebListenerRegistrar> webListenerRegistrars;
46+
private final List<WebListenerRegistrar> webListenerRegistrars;
47+
48+
private final List<CookieSameSiteSupplier> cookieSameSiteSuppliers;
4549

4650
public ServletWebServerFactoryCustomizer(ServerProperties serverProperties) {
4751
this(serverProperties, Collections.emptyList());
4852
}
4953

5054
public ServletWebServerFactoryCustomizer(ServerProperties serverProperties,
5155
List<WebListenerRegistrar> webListenerRegistrars) {
56+
this(serverProperties, webListenerRegistrars, null);
57+
}
58+
59+
ServletWebServerFactoryCustomizer(ServerProperties serverProperties,
60+
List<WebListenerRegistrar> webListenerRegistrars, List<CookieSameSiteSupplier> cookieSameSiteSuppliers) {
5261
this.serverProperties = serverProperties;
5362
this.webListenerRegistrars = webListenerRegistrars;
63+
this.cookieSameSiteSuppliers = cookieSameSiteSuppliers;
5464
}
5565

5666
@Override
@@ -77,6 +87,9 @@ public void customize(ConfigurableServletWebServerFactory factory) {
7787
for (WebListenerRegistrar registrar : this.webListenerRegistrars) {
7888
registrar.register(factory);
7989
}
90+
if (!CollectionUtils.isEmpty(this.cookieSameSiteSuppliers)) {
91+
factory.setCookieSameSiteSuppliers(this.cookieSameSiteSuppliers);
92+
}
8093
}
8194

8295
}

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfigurationTests.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@
5151
import org.springframework.boot.web.servlet.FilterRegistrationBean;
5252
import org.springframework.boot.web.servlet.ServletRegistrationBean;
5353
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
54+
import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory;
5455
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
56+
import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier;
5557
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
5658
import org.springframework.context.ApplicationContext;
5759
import org.springframework.context.annotation.Bean;
@@ -369,6 +371,14 @@ void forwardedHeaderFilterWhenFilterAlreadyRegisteredShouldBackOff() {
369371
.run((context) -> assertThat(context).hasSingleBean(FilterRegistrationBean.class));
370372
}
371373

374+
@Test
375+
void cookieSameSiteSuppliersAreApplied() {
376+
this.contextRunner.withUserConfiguration(CookieSameSiteSupplierConfiguration.class).run((context) -> {
377+
AbstractServletWebServerFactory webServerFactory = context.getBean(AbstractServletWebServerFactory.class);
378+
assertThat(webServerFactory.getCookieSameSiteSuppliers()).hasSize(2);
379+
});
380+
}
381+
372382
private ContextConsumer<AssertableWebApplicationContext> verifyContext() {
373383
return this::verifyContext;
374384
}
@@ -656,4 +666,19 @@ FilterRegistrationBean<ForwardedHeaderFilter> testForwardedHeaderFilter() {
656666

657667
}
658668

669+
@Configuration(proxyBeanMethods = false)
670+
static class CookieSameSiteSupplierConfiguration {
671+
672+
@Bean
673+
CookieSameSiteSupplier cookieSameSiteSupplier1() {
674+
return CookieSameSiteSupplier.ofLax().whenHasName("test1");
675+
}
676+
677+
@Bean
678+
CookieSameSiteSupplier cookieSameSiteSupplier2() {
679+
return CookieSameSiteSupplier.ofNone().whenHasName("test2");
680+
}
681+
682+
}
683+
659684
}

spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/servlet.adoc

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,39 @@ TIP: See the {spring-boot-autoconfigure-module-code}/web/ServerProperties.java[`
588588

589589

590590

591+
[[web.servlet.embedded-container.customizing.samesite]]
592+
===== SameSite Cookies
593+
The `SameSite` cookie attribute can be used by web browsers to control if and how cookies are submitted in cross-site requests.
594+
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.
595+
596+
If you want to change the `SameSite` attribute of your session cookie, you can use the configprop:server.servlet.session.cookie.same-site[] property.
597+
This property is supported by auto-configured Tomcat, Jetty and Undertow servers.
598+
It is also used to configure Spring Session servlet based `SessionRepository` beans.
599+
600+
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:
601+
602+
[source,yaml,indent=0,subs="verbatim",configprops,configblocks]
603+
----
604+
server:
605+
servlet:
606+
session:
607+
cookie:
608+
same-site: "none"
609+
----
610+
611+
If you want to change the `SameSite` attribute on other cookies added to your `HttpServletResponse`, you can use a `CookieSameSiteSupplier`.
612+
The `CookieSameSiteSupplier` is passed a `Cookie` and may return a `SameSite` value, or `null`.
613+
614+
There are a number of convenience factory and filter methods that you can use to quickly match specific cookies.
615+
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.*`.
616+
617+
[source,java,indent=0,subs="verbatim"]
618+
----
619+
include::{docs-java}/web/servlet/embeddedcontainer/customizing/samesite/MySameSiteConfiguration.java[]
620+
----
621+
622+
623+
591624
[[web.servlet.embedded-container.customizing.programmatic]]
592625
===== Programmatic Customization
593626
If you need to programmatically configure your embedded servlet container, you can register a Spring bean that implements the `WebServerFactoryCustomizer` interface.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright 2012-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.docs.web.servlet.embeddedcontainer.customizing.samesite;
18+
19+
import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier;
20+
import org.springframework.context.annotation.Bean;
21+
import org.springframework.context.annotation.Configuration;
22+
23+
@Configuration(proxyBeanMethods = false)
24+
public class MySameSiteConfiguration {
25+
26+
@Bean
27+
public CookieSameSiteSupplier applicationCookieSameSiteSupplier() {
28+
return CookieSameSiteSupplier.ofLax().whenHasNameMatching("myapp.*");
29+
}
30+
31+
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@
3333
import java.util.List;
3434
import java.util.Set;
3535

36+
import javax.servlet.ServletException;
37+
import javax.servlet.http.Cookie;
38+
import javax.servlet.http.HttpServletRequest;
39+
import javax.servlet.http.HttpServletResponse;
40+
import javax.servlet.http.HttpServletResponseWrapper;
41+
42+
import org.eclipse.jetty.http.HttpCookie;
3643
import org.eclipse.jetty.http.MimeTypes;
3744
import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory;
3845
import org.eclipse.jetty.server.AbstractConnector;
@@ -41,6 +48,7 @@
4148
import org.eclipse.jetty.server.Handler;
4249
import org.eclipse.jetty.server.HttpConfiguration;
4350
import org.eclipse.jetty.server.HttpConnectionFactory;
51+
import org.eclipse.jetty.server.Request;
4452
import org.eclipse.jetty.server.Server;
4553
import org.eclipse.jetty.server.ServerConnector;
4654
import org.eclipse.jetty.server.handler.ErrorHandler;
@@ -63,16 +71,19 @@
6371
import org.eclipse.jetty.webapp.Configuration;
6472
import org.eclipse.jetty.webapp.WebAppContext;
6573

74+
import org.springframework.boot.web.server.Cookie.SameSite;
6675
import org.springframework.boot.web.server.ErrorPage;
6776
import org.springframework.boot.web.server.MimeMappings;
6877
import org.springframework.boot.web.server.Shutdown;
6978
import org.springframework.boot.web.server.WebServer;
7079
import org.springframework.boot.web.servlet.ServletContextInitializer;
7180
import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory;
81+
import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier;
7282
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
7383
import org.springframework.context.ResourceLoaderAware;
7484
import org.springframework.core.io.ResourceLoader;
7585
import org.springframework.util.Assert;
86+
import org.springframework.util.CollectionUtils;
7687
import org.springframework.util.ReflectionUtils;
7788
import org.springframework.util.StringUtils;
7889

@@ -199,6 +210,9 @@ private Handler addHandlerWrappers(Handler handler) {
199210
if (StringUtils.hasText(getServerHeader())) {
200211
handler = applyWrapper(handler, JettyHandlerWrappers.createServerHeaderHandlerWrapper(getServerHeader()));
201212
}
213+
if (!CollectionUtils.isEmpty(getCookieSameSiteSuppliers())) {
214+
handler = applyWrapper(handler, new SuppliedSameSiteCookieHandlerWrapper(getCookieSameSiteSuppliers()));
215+
}
202216
return handler;
203217
}
204218

@@ -245,6 +259,10 @@ protected final void configureWebAppContext(WebAppContext context, ServletContex
245259

246260
private void configureSession(WebAppContext context) {
247261
SessionHandler handler = context.getSessionHandler();
262+
SameSite sessionSameSite = getSession().getCookie().getSameSite();
263+
if (sessionSameSite != null) {
264+
handler.setSameSite(HttpCookie.SameSite.valueOf(sessionSameSite.name()));
265+
}
248266
Duration sessionTimeout = getSession().getTimeout();
249267
handler.setMaxInactiveInterval(isNegative(sessionTimeout) ? -1 : (int) sessionTimeout.getSeconds());
250268
if (getSession().isPersistent()) {
@@ -661,4 +679,66 @@ private Class<? extends EventListener> loadClass(WebAppContext context, String c
661679

662680
}
663681

682+
/**
683+
* {@link HandlerWrapper} to apply {@link CookieSameSiteSupplier supplied}
684+
* {@link SameSite} cookie values.
685+
*/
686+
private static class SuppliedSameSiteCookieHandlerWrapper extends HandlerWrapper {
687+
688+
private final List<CookieSameSiteSupplier> suppliers;
689+
690+
SuppliedSameSiteCookieHandlerWrapper(List<CookieSameSiteSupplier> suppliers) {
691+
this.suppliers = suppliers;
692+
}
693+
694+
@Override
695+
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
696+
throws IOException, ServletException {
697+
HttpServletResponse wrappedResponse = new ResposeWrapper(response);
698+
super.handle(target, baseRequest, request, wrappedResponse);
699+
}
700+
701+
class ResposeWrapper extends HttpServletResponseWrapper {
702+
703+
ResposeWrapper(HttpServletResponse response) {
704+
super(response);
705+
}
706+
707+
@Override
708+
public void addCookie(Cookie cookie) {
709+
SameSite sameSite = getSameSite(cookie);
710+
if (sameSite != null) {
711+
String comment = HttpCookie.getCommentWithoutAttributes(cookie.getComment());
712+
String sameSiteComment = getSameSiteComment(sameSite);
713+
cookie.setComment((comment != null) ? comment + sameSiteComment : sameSiteComment);
714+
}
715+
super.addCookie(cookie);
716+
}
717+
718+
private String getSameSiteComment(SameSite sameSite) {
719+
switch (sameSite) {
720+
case NONE:
721+
return HttpCookie.SAME_SITE_NONE_COMMENT;
722+
case LAX:
723+
return HttpCookie.SAME_SITE_LAX_COMMENT;
724+
case STRICT:
725+
return HttpCookie.SAME_SITE_STRICT_COMMENT;
726+
}
727+
throw new IllegalStateException("Unsupported SameSite value " + sameSite);
728+
}
729+
730+
private SameSite getSameSite(Cookie cookie) {
731+
for (CookieSameSiteSupplier supplier : SuppliedSameSiteCookieHandlerWrapper.this.suppliers) {
732+
SameSite sameSite = supplier.getSameSite(cookie);
733+
if (sameSite != null) {
734+
return sameSite;
735+
}
736+
}
737+
return null;
738+
}
739+
740+
}
741+
742+
}
743+
664744
}

0 commit comments

Comments
 (0)