diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionAutoConfiguration.java index b5e36068a8a3..c5aadb811073 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -98,6 +98,7 @@ DefaultCookieSerializer cookieSerializer(ServerProperties serverProperties, map.from(cookie::getSecure).to(cookieSerializer::setUseSecureCookie); map.from(cookie::getMaxAge).asInt(Duration::getSeconds).to(cookieSerializer::setCookieMaxAge); map.from(cookie::getSameSite).as(SameSite::attributeValue).to(cookieSerializer::setSameSite); + map.from(cookie::getPartitioned).to(cookieSerializer::setPartitioned); cookieSerializerCustomizers.orderedStream().forEach((customizer) -> customizer.customize(cookieSerializer)); return cookieSerializer; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebSessionIdResolverAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebSessionIdResolverAutoConfiguration.java index 0ef46418d57c..797910b8ea37 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebSessionIdResolverAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebSessionIdResolverAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2024 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. @@ -76,6 +76,7 @@ private void initializeCookie(ResponseCookieBuilder builder) { map.from(cookie::getHttpOnly).to(builder::httpOnly); map.from(cookie::getSecure).to(builder::secure); map.from(cookie::getMaxAge).to(builder::maxAge); + map.from(cookie::getPartitioned).to(builder::partitioned); map.from(getSameSite(cookie)).to(builder::sameSite); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 63dae453db94..4636c60190ce 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -143,6 +143,10 @@ "name": "server.reactive.session.cookie.name", "description": "Name for the cookie." }, + { + "name": "server.reactive.session.cookie.partitioned", + "description": "Whether the generated cookie carries the Partitioned attribute." + }, { "name": "server.reactive.session.cookie.path", "description": "Path of the cookie." @@ -229,6 +233,10 @@ "name": "server.servlet.session.cookie.name", "description": "Name of the cookie." }, + { + "name": "server.servlet.session.cookie.partitioned", + "description": "Whether the generated cookie carries the Partitioned attribute." + }, { "name": "server.servlet.session.cookie.path", "description": "Path of the cookie." diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationTests.java index eabb3a4655c6..392ec235aeac 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationTests.java @@ -156,7 +156,7 @@ void sessionCookieConfigurationIsAppliedToAutoConfiguredCookieSerializer() { .withPropertyValues("server.servlet.session.cookie.name=sid", "server.servlet.session.cookie.domain=spring", "server.servlet.session.cookie.path=/test", "server.servlet.session.cookie.httpOnly=false", "server.servlet.session.cookie.secure=false", "server.servlet.session.cookie.maxAge=10s", - "server.servlet.session.cookie.sameSite=strict") + "server.servlet.session.cookie.sameSite=strict", "server.servlet.session.cookie.partitioned=true") .run((context) -> { DefaultCookieSerializer cookieSerializer = context.getBean(DefaultCookieSerializer.class); assertThat(cookieSerializer).hasFieldOrPropertyWithValue("cookieName", "sid"); @@ -166,6 +166,7 @@ void sessionCookieConfigurationIsAppliedToAutoConfiguredCookieSerializer() { assertThat(cookieSerializer).hasFieldOrPropertyWithValue("useSecureCookie", false); assertThat(cookieSerializer).hasFieldOrPropertyWithValue("cookieMaxAge", 10); assertThat(cookieSerializer).hasFieldOrPropertyWithValue("sameSite", "Strict"); + assertThat(cookieSerializer).hasFieldOrPropertyWithValue("partitioned", true); }); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java index 27ae96ee440e..0c33a4dacd44 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java @@ -644,7 +644,8 @@ void customSessionCookieConfigurationShouldBeApplied() { this.contextRunner.withPropertyValues("server.reactive.session.cookie.name:JSESSIONID", "server.reactive.session.cookie.domain:.example.com", "server.reactive.session.cookie.path:/example", "server.reactive.session.cookie.max-age:60", "server.reactive.session.cookie.http-only:false", - "server.reactive.session.cookie.secure:false", "server.reactive.session.cookie.same-site:strict") + "server.reactive.session.cookie.secure:false", "server.reactive.session.cookie.same-site:strict", + "server.reactive.session.cookie.partitioned:true") .run(assertExchangeWithSession((exchange) -> { List cookies = exchange.getResponse().getCookies().get("JSESSIONID"); assertThat(cookies).isNotEmpty(); @@ -654,6 +655,7 @@ void customSessionCookieConfigurationShouldBeApplied() { assertThat(cookies).allMatch((cookie) -> !cookie.isHttpOnly()); assertThat(cookies).allMatch((cookie) -> !cookie.isSecure()); assertThat(cookies).allMatch((cookie) -> cookie.getSameSite().equals("Strict")); + assertThat(cookies).allMatch(ResponseCookie::isPartitioned); })); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Cookie.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Cookie.java index cb47e7f33e5c..dcf8721610b5 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Cookie.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Cookie.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2024 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. @@ -57,6 +57,11 @@ public class Cookie { */ private Boolean secure; + /** + * Whether the generated cookie carries the Partitioned attribute. + */ + private Boolean partitioned; + /** * Maximum age of the cookie. If a duration suffix is not specified, seconds will be * used. A positive value indicates when the cookie expires relative to the current @@ -127,6 +132,14 @@ public void setSameSite(SameSite sameSite) { this.sameSite = sameSite; } + public Boolean getPartitioned() { + return this.partitioned; + } + + public void setPartitioned(Boolean partitioned) { + this.partitioned = partitioned; + } + /** * SameSite values. */ diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactory.java index 4f3ef502c9d4..8f00ee02f53c 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactory.java @@ -60,6 +60,8 @@ public abstract class AbstractServletWebServerFactory extends AbstractConfigurableWebServerFactory implements ConfigurableServletWebServerFactory { + static final String PARTITIONED_ATTRIBUTE_NAME = "Partitioned"; + protected final Log logger = LogFactory.getLog(getClass()); private String contextPath = ""; @@ -350,6 +352,9 @@ private void configureSessionCookie(SessionCookieConfig config) { map.from(cookie::getHttpOnly).to(config::setHttpOnly); map.from(cookie::getSecure).to(config::setSecure); map.from(cookie::getMaxAge).asInt(Duration::getSeconds).to(config::setMaxAge); + map.from(cookie::getPartitioned) + .as(Object::toString) + .to((partitioned) -> config.setAttribute(PARTITIONED_ATTRIBUTE_NAME, partitioned)); } private Set unwrap(Set modes) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java index 9219de35c0fb..0e46e4e77578 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java @@ -863,6 +863,7 @@ void sessionCookieConfiguration() { factory.getSession().getCookie().setPath("/testpath"); factory.getSession().getCookie().setHttpOnly(true); factory.getSession().getCookie().setSecure(true); + factory.getSession().getCookie().setPartitioned(true); factory.getSession().getCookie().setMaxAge(Duration.ofSeconds(60)); final AtomicReference configReference = new AtomicReference<>(); this.webServer = factory.getWebServer((context) -> configReference.set(context.getSessionCookieConfig())); @@ -872,6 +873,8 @@ void sessionCookieConfiguration() { assertThat(sessionCookieConfig.getPath()).isEqualTo("/testpath"); assertThat(sessionCookieConfig.isHttpOnly()).isTrue(); assertThat(sessionCookieConfig.isSecure()).isTrue(); + assertThat(sessionCookieConfig.getAttribute(AbstractServletWebServerFactory.PARTITIONED_ATTRIBUTE_NAME)) + .isEqualTo("true"); assertThat(sessionCookieConfig.getMaxAge()).isEqualTo(60); } @@ -1166,6 +1169,7 @@ void sessionConfiguration() { factory.getSession().getCookie().setPath("/testpath"); factory.getSession().getCookie().setHttpOnly(true); factory.getSession().getCookie().setSecure(true); + factory.getSession().getCookie().setPartitioned(false); factory.getSession().getCookie().setMaxAge(Duration.ofMinutes(1)); AtomicReference contextReference = new AtomicReference<>(); factory.getWebServer(contextReference::set).start(); @@ -1178,6 +1182,8 @@ void sessionConfiguration() { assertThat(servletContext.getSessionCookieConfig().isHttpOnly()).isTrue(); assertThat(servletContext.getSessionCookieConfig().isSecure()).isTrue(); assertThat(servletContext.getSessionCookieConfig().getMaxAge()).isEqualTo(60); + assertThat(servletContext.getSessionCookieConfig() + .getAttribute(AbstractServletWebServerFactory.PARTITIONED_ATTRIBUTE_NAME)).isEqualTo("false"); } @Test