diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/MongoReactiveSessionConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/MongoReactiveSessionConfiguration.java index 73ecbf0119..de5fadcd72 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/MongoReactiveSessionConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/MongoReactiveSessionConfiguration.java @@ -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. @@ -22,6 +22,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; @@ -34,6 +35,7 @@ import org.springframework.session.data.mongo.config.annotation.web.reactive.Rea * Mongo-backed reactive session configuration. * * @author Andy Wilkinson + * @author Weix Sun */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ ReactiveMongoOperations.class, ReactiveMongoSessionRepository.class }) @@ -47,8 +49,10 @@ class MongoReactiveSessionConfiguration { static class SpringBootReactiveMongoWebSessionConfiguration extends ReactiveMongoWebSessionConfiguration { @Autowired - void customize(SessionProperties sessionProperties, MongoSessionProperties mongoSessionProperties) { - Duration timeout = sessionProperties.getTimeout(); + void customize(SessionProperties sessionProperties, MongoSessionProperties mongoSessionProperties, + ServerProperties serverProperties) { + Duration timeout = sessionProperties + .determineTimeout(() -> serverProperties.getReactive().getSession().getTimeout()); if (timeout != null) { setMaxInactiveIntervalInSeconds((int) timeout.getSeconds()); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/RedisReactiveSessionConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/RedisReactiveSessionConfiguration.java index 15edb73b3b..b807f93bd7 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/RedisReactiveSessionConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/RedisReactiveSessionConfiguration.java @@ -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. @@ -22,6 +22,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; @@ -34,6 +35,7 @@ import org.springframework.session.data.redis.config.annotation.web.server.Redis * Redis-backed reactive session configuration. * * @author Andy Wilkinson + * @author Weix Sun */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ ReactiveRedisConnectionFactory.class, ReactiveRedisSessionRepository.class }) @@ -47,8 +49,10 @@ class RedisReactiveSessionConfiguration { static class SpringBootRedisWebSessionConfiguration extends RedisWebSessionConfiguration { @Autowired - void customize(SessionProperties sessionProperties, RedisSessionProperties redisSessionProperties) { - Duration timeout = sessionProperties.getTimeout(); + void customize(SessionProperties sessionProperties, RedisSessionProperties redisSessionProperties, + ServerProperties serverProperties) { + Duration timeout = sessionProperties + .determineTimeout(() -> serverProperties.getReactive().getSession().getTimeout()); if (timeout != null) { setMaxInactiveIntervalInSeconds((int) timeout.getSeconds()); } 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 0c055004e9..c86df6e0c6 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 @@ -16,6 +16,7 @@ package org.springframework.boot.autoconfigure.session; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -43,6 +44,8 @@ import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration; import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration; +import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties; +import org.springframework.boot.autoconfigure.web.reactive.WebSessionIdResolverAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.web.servlet.server.Session.Cookie; @@ -71,15 +74,17 @@ import org.springframework.session.web.http.HttpSessionIdResolver; * @author Eddú Meléndez * @author Stephane Nicoll * @author Vedran Pavic + * @author Weix Sun * @since 1.4.0 */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(Session.class) @ConditionalOnWebApplication -@EnableConfigurationProperties({ ServerProperties.class, SessionProperties.class }) +@EnableConfigurationProperties({ ServerProperties.class, SessionProperties.class, WebFluxProperties.class }) @AutoConfigureAfter({ DataSourceAutoConfiguration.class, HazelcastAutoConfiguration.class, JdbcTemplateAutoConfiguration.class, MongoDataAutoConfiguration.class, MongoReactiveDataAutoConfiguration.class, - RedisAutoConfiguration.class, RedisReactiveAutoConfiguration.class }) + RedisAutoConfiguration.class, RedisReactiveAutoConfiguration.class, + WebSessionIdResolverAutoConfiguration.class }) @AutoConfigureBefore({ HttpHandlerAutoConfiguration.class, WebFluxAutoConfiguration.class }) public class SessionAutoConfiguration { @@ -100,7 +105,7 @@ public class SessionAutoConfiguration { map.from(cookie::getPath).to(cookieSerializer::setCookiePath); map.from(cookie::getHttpOnly).to(cookieSerializer::setUseHttpOnlyCookie); map.from(cookie::getSecure).to(cookieSerializer::setUseSecureCookie); - map.from(cookie::getMaxAge).to((maxAge) -> cookieSerializer.setCookieMaxAge((int) maxAge.getSeconds())); + map.from(cookie::getMaxAge).asInt(Duration::getSeconds).to(cookieSerializer::setCookieMaxAge); 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/ServerProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java index 9de2e454b2..69791d1048 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java @@ -34,6 +34,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.NestedConfigurationProperty; import org.springframework.boot.convert.DurationUnit; import org.springframework.boot.web.server.Compression; +import org.springframework.boot.web.server.Cookie; import org.springframework.boot.web.server.Http2; import org.springframework.boot.web.server.Shutdown; import org.springframework.boot.web.server.Ssl; @@ -116,6 +117,8 @@ public class ServerProperties { private final Servlet servlet = new Servlet(); + private final Reactive reactive = new Reactive(); + private final Tomcat tomcat = new Tomcat(); private final Jetty jetty = new Jetty(); @@ -188,6 +191,10 @@ public class ServerProperties { return this.servlet; } + public Reactive getReactive() { + return this.reactive; + } + public Tomcat getTomcat() { return this.tomcat; } @@ -213,7 +220,7 @@ public class ServerProperties { } /** - * Servlet properties. + * Servlet server properties. */ public static class Servlet { @@ -296,6 +303,44 @@ public class ServerProperties { } + /** + * Reactive server properties. + */ + public static class Reactive { + + private final Session session = new Session(); + + public Session getSession() { + return this.session; + } + + public static class Session { + + /** + * Session timeout. If a duration suffix is not specified, seconds will be + * used. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration timeout = Duration.ofMinutes(30); + + private final Cookie cookie = new Cookie(); + + public Duration getTimeout() { + return this.timeout; + } + + public void setTimeout(Duration timeout) { + this.timeout = timeout; + } + + public Cookie getCookie() { + return this.cookie; + } + + } + + } + /** * Tomcat properties. */ diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java index d9ce69ead6..b8df080f24 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java @@ -17,10 +17,10 @@ package org.springframework.boot.autoconfigure.web.reactive; import java.time.Duration; -import java.util.function.Supplier; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Mono; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.ObjectProvider; @@ -36,6 +36,7 @@ import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvi import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.autoconfigure.validation.ValidatorAdapter; import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain; +import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.autoconfigure.web.WebProperties; import org.springframework.boot.autoconfigure.web.WebProperties.Resources; import org.springframework.boot.autoconfigure.web.format.DateTimeFormatters; @@ -72,12 +73,13 @@ import org.springframework.web.reactive.result.method.annotation.ArgumentResolve import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.reactive.result.view.ViewResolver; +import org.springframework.web.server.WebSession; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver; import org.springframework.web.server.i18n.FixedLocaleContextResolver; import org.springframework.web.server.i18n.LocaleContextResolver; -import org.springframework.web.server.session.CookieWebSessionIdResolver; import org.springframework.web.server.session.DefaultWebSessionManager; +import org.springframework.web.server.session.InMemoryWebSessionStore; import org.springframework.web.server.session.WebSessionIdResolver; import org.springframework.web.server.session.WebSessionManager; @@ -92,6 +94,7 @@ import org.springframework.web.server.session.WebSessionManager; * @author Eddú Meléndez * @author Artsiom Yudovin * @author Chris Bono + * @author Weix Sun * @since 2.0.0 */ @Configuration(proxyBeanMethods = false) @@ -99,7 +102,8 @@ import org.springframework.web.server.session.WebSessionManager; @ConditionalOnClass(WebFluxConfigurer.class) @ConditionalOnMissingBean({ WebFluxConfigurationSupport.class }) @AutoConfigureAfter({ ReactiveWebServerFactoryAutoConfiguration.class, CodecsAutoConfiguration.class, - ReactiveMultipartAutoConfiguration.class, ValidationAutoConfiguration.class }) + ReactiveMultipartAutoConfiguration.class, ValidationAutoConfiguration.class, + WebSessionIdResolverAutoConfiguration.class }) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10) public class WebFluxAutoConfiguration { @@ -231,19 +235,22 @@ public class WebFluxAutoConfiguration { * Configuration equivalent to {@code @EnableWebFlux}. */ @Configuration(proxyBeanMethods = false) - @EnableConfigurationProperties(WebProperties.class) + @EnableConfigurationProperties({ WebProperties.class, ServerProperties.class }) public static class EnableWebFluxConfiguration extends DelegatingWebFluxConfiguration { private final WebFluxProperties webFluxProperties; private final WebProperties webProperties; + private final ServerProperties serverProperties; + private final WebFluxRegistrations webFluxRegistrations; public EnableWebFluxConfiguration(WebFluxProperties webFluxProperties, WebProperties webProperties, - ObjectProvider webFluxRegistrations) { + ServerProperties serverProperties, ObjectProvider webFluxRegistrations) { this.webFluxProperties = webFluxProperties; this.webProperties = webProperties; + this.serverProperties = serverProperties; this.webFluxRegistrations = webFluxRegistrations.getIfUnique(); } @@ -304,19 +311,12 @@ public class WebFluxAutoConfiguration { @ConditionalOnMissingBean(name = WebHttpHandlerBuilder.WEB_SESSION_MANAGER_BEAN_NAME) public WebSessionManager webSessionManager(ObjectProvider webSessionIdResolver) { DefaultWebSessionManager webSessionManager = new DefaultWebSessionManager(); - webSessionManager.setSessionIdResolver(webSessionIdResolver.getIfAvailable(cookieWebSessionIdResolver())); + Duration timeout = this.serverProperties.getReactive().getSession().getTimeout(); + webSessionManager.setSessionStore(new MaxIdleTimeInMemoryWebSessionStore(timeout)); + webSessionIdResolver.ifAvailable(webSessionManager::setSessionIdResolver); return webSessionManager; } - private Supplier cookieWebSessionIdResolver() { - return () -> { - CookieWebSessionIdResolver webSessionIdResolver = new CookieWebSessionIdResolver(); - webSessionIdResolver.addCookieInitializer((cookie) -> cookie - .sameSite(this.webFluxProperties.getSession().getCookie().getSameSite().attribute())); - return webSessionIdResolver; - }; - } - } @Configuration(proxyBeanMethods = false) @@ -331,4 +331,23 @@ public class WebFluxAutoConfiguration { } + static final class MaxIdleTimeInMemoryWebSessionStore extends InMemoryWebSessionStore { + + private final Duration timeout; + + private MaxIdleTimeInMemoryWebSessionStore(Duration timeout) { + this.timeout = timeout; + } + + @Override + public Mono createWebSession() { + return super.createWebSession().doOnSuccess(this::setMaxIdleTime); + } + + private void setMaxIdleTime(WebSession session) { + session.setMaxIdleTime(this.timeout); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxProperties.java index a04fd0aada..48d68e0b3b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxProperties.java @@ -17,6 +17,7 @@ package org.springframework.boot.autoconfigure.web.reactive; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; import org.springframework.util.StringUtils; /** @@ -67,6 +68,7 @@ public class WebFluxProperties { return this.format; } + @DeprecatedConfigurationProperty(replacement = "server.reactive.session") public Session getSession() { return this.session; } @@ -122,23 +124,39 @@ public class WebFluxProperties { } + /** + * Session properties. + * + * @deprecated since 2.6.0 for removal in 2.8.0 in favor of + * {@code server.reactive.session}. + */ + @Deprecated public static class Session { private final Cookie cookie = new Cookie(); + @DeprecatedConfigurationProperty(replacement = "server.reactive.session.cookie") public Cookie getCookie() { return this.cookie; } } + /** + * Session cookie properties. + * + * @deprecated since 2.6.0 for removal in 2.8.0 in favor of + * {@link org.springframework.boot.web.server.Cookie}. + */ + @Deprecated public static class Cookie { /** * SameSite attribute value for session Cookies. */ - private SameSite sameSite = SameSite.LAX; + private SameSite sameSite; + @DeprecatedConfigurationProperty(replacement = "server.reactive.session.cookie.same-site") public SameSite getSameSite() { return this.sameSite; } @@ -149,6 +167,12 @@ public class WebFluxProperties { } + /** + * SameSite values. + * @deprecated since 2.6.0 for removal in 2.8.0 in favor of + * {@link org.springframework.boot.web.server.Cookie.SameSite}. + */ + @Deprecated public enum SameSite { /** 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 new file mode 100644 index 0000000000..adf1b6f416 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebSessionIdResolverAutoConfiguration.java @@ -0,0 +1,106 @@ +/* + * 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.autoconfigure.web.reactive; + +import reactor.core.publisher.Mono; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.context.properties.source.MutuallyExclusiveConfigurationPropertiesException; +import org.springframework.boot.web.server.Cookie; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.ResponseCookie.ResponseCookieBuilder; +import org.springframework.util.StringUtils; +import org.springframework.web.server.session.CookieWebSessionIdResolver; +import org.springframework.web.server.session.WebSessionIdResolver; +import org.springframework.web.server.session.WebSessionManager; + +/** + * Auto-configuration for {@link WebSessionIdResolver}. + * + * @author Phillip Webb + * @author Brian Clozel + * @author Weix Sun + * @since 2.6.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass({ WebSessionManager.class, Mono.class }) +@EnableConfigurationProperties({ WebFluxProperties.class, ServerProperties.class }) +public class WebSessionIdResolverAutoConfiguration { + + private final ServerProperties serverProperties; + + private final WebFluxProperties webFluxProperties; + + public WebSessionIdResolverAutoConfiguration(ServerProperties serverProperties, + WebFluxProperties webFluxProperties) { + this.serverProperties = serverProperties; + this.webFluxProperties = webFluxProperties; + assertNoMutuallyExclusiveProperties(serverProperties, webFluxProperties); + } + + @SuppressWarnings("deprecation") + private void assertNoMutuallyExclusiveProperties(ServerProperties serverProperties, + WebFluxProperties webFluxProperties) { + MutuallyExclusiveConfigurationPropertiesException.throwIfMultipleNonNullValuesIn((entries) -> { + entries.put("spring.webflux.session.cookie.same-site", + webFluxProperties.getSession().getCookie().getSameSite()); + entries.put("server.reactive.session.cookie.same-site", + serverProperties.getReactive().getSession().getCookie().getSameSite()); + }); + } + + @Bean + @ConditionalOnMissingBean + public WebSessionIdResolver webSessionIdResolver() { + CookieWebSessionIdResolver resolver = new CookieWebSessionIdResolver(); + String cookieName = this.serverProperties.getReactive().getSession().getCookie().getName(); + if (StringUtils.hasText(cookieName)) { + resolver.setCookieName(cookieName); + } + resolver.addCookieInitializer(this::initializeCookie); + return resolver; + } + + private void initializeCookie(ResponseCookieBuilder builder) { + Cookie cookie = this.serverProperties.getReactive().getSession().getCookie(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(cookie::getDomain).to(builder::domain); + map.from(cookie::getPath).to(builder::path); + map.from(cookie::getHttpOnly).to(builder::httpOnly); + map.from(cookie::getSecure).to(builder::secure); + map.from(cookie::getMaxAge).to(builder::maxAge); + map.from(getSameSite(cookie)).to(builder::sameSite); + } + + @SuppressWarnings("deprecation") + private String getSameSite(Cookie properties) { + if (properties.getSameSite() != null) { + return properties.getSameSite().attributeValue(); + } + WebFluxProperties.Cookie deprecatedProperties = this.webFluxProperties.getSession().getCookie(); + if (deprecatedProperties.getSameSite() != null) { + return deprecatedProperties.getSameSite().attribute(); + } + return null; + } + +} 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 607d04177e..9b5f00bcc5 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 @@ -158,34 +158,6 @@ "level": "error" } }, - { - "name": "server.servlet.session.cookie.comment", - "description": "Comment for the session cookie." - }, - { - "name": "server.servlet.session.cookie.domain", - "description": "Domain for the session cookie." - }, - { - "name": "server.servlet.session.cookie.http-only", - "description": "Whether to use \"HttpOnly\" cookies for session cookies." - }, - { - "name": "server.servlet.session.cookie.max-age", - "description": "Maximum age of the session cookie. If a duration suffix is not specified, seconds will be used." - }, - { - "name": "server.servlet.session.cookie.name", - "description": "Session cookie name." - }, - { - "name": "server.servlet.session.cookie.path", - "description": "Path of the session cookie." - }, - { - "name": "server.servlet.session.cookie.secure", - "description": "Whether to always mark the session cookie as secure." - }, { "name": "server.servlet.session.persistent", "description": "Whether to persist session data between restarts.", @@ -2028,8 +2000,8 @@ "defaultValue": false }, { - "name": "spring.webflux.session.cookie.same-site", - "defaultValue": "lax" + "name": "spring.webflux.session.timeout", + "defaultValue": "30m" }, { "name": "spring.webservices.wsdl-locations", diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index 3f22befcdd..e368ec72bd 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -141,6 +141,7 @@ org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration org.springframework.boot.autoconfigure.web.reactive.ReactiveMultipartAutoConfiguration,\ org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration,\ org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,\ +org.springframework.boot.autoconfigure.web.reactive.WebSessionIdResolverAutoConfiguration,\ org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration,\ org.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorAutoConfiguration,\ org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration,\ diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/AbstractSessionAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/AbstractSessionAutoConfigurationTests.java index 510beb70c1..a01c6b534b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/AbstractSessionAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/AbstractSessionAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 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. @@ -17,16 +17,23 @@ package org.springframework.boot.autoconfigure.session; import java.util.Collections; +import java.util.function.Consumer; +import org.springframework.boot.autoconfigure.web.reactive.MockReactiveWebServerFactory; import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext; import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; +import org.springframework.boot.test.context.runner.ContextConsumer; +import org.springframework.boot.web.reactive.context.ReactiveWebApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; import org.springframework.session.MapSessionRepository; import org.springframework.session.ReactiveSessionRepository; import org.springframework.session.SessionRepository; import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession; import org.springframework.session.web.http.SessionRepositoryFilter; +import org.springframework.web.server.WebSession; import org.springframework.web.server.session.WebSessionManager; import static org.assertj.core.api.Assertions.assertThat; @@ -35,9 +42,25 @@ import static org.assertj.core.api.Assertions.assertThat; * Shared test utilities for {@link SessionAutoConfiguration} tests. * * @author Stephane Nicoll + * @author Weix Sun */ public abstract class AbstractSessionAutoConfigurationTests { + private static final MockReactiveWebServerFactory mockReactiveWebServerFactory = new MockReactiveWebServerFactory(); + + protected ContextConsumer assertExchangeWithSession( + Consumer exchange) { + return (context) -> { + MockServerHttpRequest request = MockServerHttpRequest.get("/").build(); + MockServerWebExchange webExchange = MockServerWebExchange.from(request); + WebSessionManager webSessionManager = context.getBean(WebSessionManager.class); + WebSession webSession = webSessionManager.getSession(webExchange).block(); + webSession.start(); + webExchange.getResponse().setComplete().block(); + exchange.accept(webExchange); + }; + } + protected > T validateSessionRepository(AssertableWebApplicationContext context, Class type) { assertThat(context).hasSingleBean(SessionRepositoryFilter.class); @@ -67,4 +90,14 @@ public abstract class AbstractSessionAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + MockReactiveWebServerFactory mockReactiveWebServerFactory() { + return mockReactiveWebServerFactory; + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/ReactiveSessionAutoConfigurationMongoTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/ReactiveSessionAutoConfigurationMongoTests.java index ea63167f4a..106f53339b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/ReactiveSessionAutoConfigurationMongoTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/ReactiveSessionAutoConfigurationMongoTests.java @@ -16,6 +16,9 @@ package org.springframework.boot.autoconfigure.session; +import java.time.Duration; +import java.util.List; + import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -24,10 +27,12 @@ import org.springframework.boot.autoconfigure.data.mongo.MongoReactiveDataAutoCo import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration; +import org.springframework.boot.autoconfigure.web.reactive.WebSessionIdResolverAutoConfiguration; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext; import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; +import org.springframework.http.ResponseCookie; import org.springframework.session.data.mongo.ReactiveMongoSessionRepository; import org.springframework.session.data.redis.ReactiveRedisSessionRepository; @@ -37,6 +42,7 @@ import static org.assertj.core.api.Assertions.assertThat; * Mongo-specific tests for {@link SessionAutoConfiguration}. * * @author Andy Wilkinson + * @author Weix Sun */ class ReactiveSessionAutoConfigurationMongoTests extends AbstractSessionAutoConfigurationTests { @@ -75,6 +81,19 @@ class ReactiveSessionAutoConfigurationMongoTests extends AbstractSessionAutoConf }); } + @Test + void defaultConfigWithCustomSessionTimeout() { + this.contextRunner.withPropertyValues("spring.session.store-type=mongodb", "server.reactive.session.timeout=1m") + .withConfiguration(AutoConfigurations.of(EmbeddedMongoAutoConfiguration.class, + MongoAutoConfiguration.class, MongoDataAutoConfiguration.class, + MongoReactiveAutoConfiguration.class, MongoReactiveDataAutoConfiguration.class)) + .run((context) -> { + ReactiveMongoSessionRepository repository = validateSessionRepository(context, + ReactiveMongoSessionRepository.class); + assertThat(repository).hasFieldOrPropertyWithValue("maxInactiveIntervalInSeconds", 60); + }); + } + @Test void mongoSessionStoreWithCustomizations() { this.contextRunner @@ -85,6 +104,30 @@ class ReactiveSessionAutoConfigurationMongoTests extends AbstractSessionAutoConf .run(validateSpringSessionUsesMongo("foo")); } + @Test + void sessionCookieConfigurationIsAppliedToAutoConfiguredWebSessionIdResolver() { + AutoConfigurations autoConfigurations = AutoConfigurations.of(EmbeddedMongoAutoConfiguration.class, + MongoAutoConfiguration.class, MongoDataAutoConfiguration.class, MongoReactiveAutoConfiguration.class, + MongoReactiveDataAutoConfiguration.class, WebSessionIdResolverAutoConfiguration.class); + this.contextRunner.withConfiguration(autoConfigurations).withUserConfiguration(Config.class) + .withPropertyValues("spring.session.store-type=mongodb", + "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") + .run(assertExchangeWithSession((exchange) -> { + List cookies = exchange.getResponse().getCookies().get("JSESSIONID"); + assertThat(cookies).isNotEmpty(); + assertThat(cookies).allMatch((cookie) -> cookie.getDomain().equals(".example.com")); + assertThat(cookies).allMatch((cookie) -> cookie.getPath().equals("/example")); + assertThat(cookies).allMatch((cookie) -> cookie.getMaxAge().equals(Duration.ofSeconds(60))); + assertThat(cookies).allMatch((cookie) -> !cookie.isHttpOnly()); + assertThat(cookies).allMatch((cookie) -> !cookie.isSecure()); + assertThat(cookies).allMatch((cookie) -> cookie.getSameSite().equals("Strict")); + })); + } + private ContextConsumer validateSpringSessionUsesMongo( String collectionName) { return (context) -> { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/ReactiveSessionAutoConfigurationRedisTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/ReactiveSessionAutoConfigurationRedisTests.java index c85272dd15..89a04478d6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/ReactiveSessionAutoConfigurationRedisTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/ReactiveSessionAutoConfigurationRedisTests.java @@ -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. @@ -16,7 +16,12 @@ package org.springframework.boot.autoconfigure.session; +import java.time.Duration; +import java.util.List; + import org.junit.jupiter.api.Test; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; @@ -25,6 +30,8 @@ import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext; import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; +import org.springframework.boot.testsupport.testcontainers.RedisContainer; +import org.springframework.http.ResponseCookie; import org.springframework.session.MapSession; import org.springframework.session.SaveMode; import org.springframework.session.data.mongo.ReactiveMongoSessionRepository; @@ -38,9 +45,15 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Stephane Nicoll * @author Andy Wilkinson * @author Vedran Pavic + * @author Weix Sun */ +@Testcontainers(disabledWithoutDocker = true) class ReactiveSessionAutoConfigurationRedisTests extends AbstractSessionAutoConfigurationTests { + @Container + public static RedisContainer redis = new RedisContainer().withStartupAttempts(5) + .withStartupTimeout(Duration.ofMinutes(10)); + protected final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(SessionAutoConfiguration.class)); @@ -72,6 +85,18 @@ class ReactiveSessionAutoConfigurationRedisTests extends AbstractSessionAutoConf }); } + @Test + void defaultConfigWithCustomWebFluxTimeout() { + this.contextRunner.withPropertyValues("spring.session.store-type=redis", "spring.webflux.session.timeout=1m") + .withConfiguration( + AutoConfigurations.of(RedisAutoConfiguration.class, RedisReactiveAutoConfiguration.class)) + .run((context) -> { + ReactiveRedisSessionRepository repository = validateSessionRepository(context, + ReactiveRedisSessionRepository.class); + assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval", 60); + }); + } + @Test void redisSessionStoreWithCustomizations() { this.contextRunner @@ -82,6 +107,31 @@ class ReactiveSessionAutoConfigurationRedisTests extends AbstractSessionAutoConf .run(validateSpringSessionUsesRedis("foo:", SaveMode.ON_GET_ATTRIBUTE)); } + @Test + void sessionCookieConfigurationIsAppliedToAutoConfiguredWebSessionIdResolver() { + this.contextRunner + .withConfiguration( + AutoConfigurations.of(RedisAutoConfiguration.class, RedisReactiveAutoConfiguration.class)) + .withUserConfiguration(Config.class) + .withPropertyValues("spring.session.store-type=redis", "spring.redis.host=" + redis.getHost(), + "spring.redis.port=" + redis.getFirstMappedPort(), "spring.session.store-type=redis", + "spring.webflux.session.cookie.name:JSESSIONID", + "spring.webflux.session.cookie.domain:.example.com", + "spring.webflux.session.cookie.path:/example", "spring.webflux.session.cookie.max-age:60", + "spring.webflux.session.cookie.http-only:false", "spring.webflux.session.cookie.secure:false", + "spring.webflux.session.cookie.same-site:strict") + .run(assertExchangeWithSession((exchange) -> { + List cookies = exchange.getResponse().getCookies().get("JSESSIONID"); + assertThat(cookies).isNotEmpty(); + assertThat(cookies).allMatch((cookie) -> cookie.getDomain().equals(".example.com")); + assertThat(cookies).allMatch((cookie) -> cookie.getPath().equals("/example")); + assertThat(cookies).allMatch((cookie) -> cookie.getMaxAge().equals(Duration.ofSeconds(60))); + assertThat(cookies).allMatch((cookie) -> !cookie.isHttpOnly()); + assertThat(cookies).allMatch((cookie) -> !cookie.isSecure()); + assertThat(cookies).allMatch((cookie) -> cookie.getSameSite().equals("Strict")); + })); + } + private ContextConsumer validateSpringSessionUsesRedis(String namespace, SaveMode saveMode) { return (context) -> { 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 59edb80ea7..30a9cf9bfd 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 @@ -16,6 +16,7 @@ package org.springframework.boot.autoconfigure.web.reactive; +import java.time.Duration; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; @@ -39,6 +40,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.autoconfigure.validation.ValidatorAdapter; import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration.WebFluxConfig; +import org.springframework.boot.context.properties.source.MutuallyExclusiveConfigurationPropertiesException; import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.boot.web.codec.CodecCustomizer; @@ -56,6 +58,7 @@ import org.springframework.format.Parser; import org.springframework.format.Printer; import org.springframework.format.support.FormattingConversionService; import org.springframework.http.CacheControl; +import org.springframework.http.ResponseCookie; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; @@ -109,7 +112,8 @@ class WebFluxAutoConfigurationTests { private static final MockReactiveWebServerFactory mockReactiveWebServerFactory = new MockReactiveWebServerFactory(); private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class)) + .withConfiguration( + AutoConfigurations.of(WebFluxAutoConfiguration.class, WebSessionIdResolverAutoConfiguration.class)) .withUserConfiguration(Config.class); @Test @@ -568,12 +572,49 @@ class WebFluxAutoConfigurationTests { } @Test - void customSameSiteConfigurationShouldBeApplied() { + void customSessionTimeoutConfigurationShouldBeApplied() { + this.contextRunner.withPropertyValues("server.reactive.session.timeout:123") + .run((assertSessionTimeoutWithWebSession((webSession) -> { + webSession.start(); + assertThat(webSession.getMaxIdleTime()).hasSeconds(123); + }))); + } + + @Test + void sameSiteAttributesAreExclusive() { + this.contextRunner.withPropertyValues("spring.webflux.session.cookie.same-site:strict", + "server.reactive.session.cookie.same-site:strict").run((context) -> { + assertThat(context).hasFailed(); + assertThat(context).getFailure() + .hasRootCauseExactlyInstanceOf(MutuallyExclusiveConfigurationPropertiesException.class); + }); + } + + @Test + void deprecatedCustomSameSiteConfigurationShouldBeApplied() { this.contextRunner.withPropertyValues("spring.webflux.session.cookie.same-site:strict").run( assertExchangeWithSession((exchange) -> assertThat(exchange.getResponse().getCookies().get("SESSION")) .isNotEmpty().allMatch((cookie) -> cookie.getSameSite().equals("Strict")))); } + @Test + 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") + .run(assertExchangeWithSession((exchange) -> { + List cookies = exchange.getResponse().getCookies().get("JSESSIONID"); + assertThat(cookies).isNotEmpty(); + assertThat(cookies).allMatch((cookie) -> cookie.getDomain().equals(".example.com")); + assertThat(cookies).allMatch((cookie) -> cookie.getPath().equals("/example")); + assertThat(cookies).allMatch((cookie) -> cookie.getMaxAge().equals(Duration.ofSeconds(60))); + assertThat(cookies).allMatch((cookie) -> !cookie.isHttpOnly()); + assertThat(cookies).allMatch((cookie) -> !cookie.isSecure()); + assertThat(cookies).allMatch((cookie) -> cookie.getSameSite().equals("Strict")); + })); + } + private ContextConsumer assertExchangeWithSession( Consumer exchange) { return (context) -> { @@ -587,6 +628,17 @@ class WebFluxAutoConfigurationTests { }; } + private ContextConsumer assertSessionTimeoutWithWebSession( + Consumer session) { + return (context) -> { + MockServerHttpRequest request = MockServerHttpRequest.get("/").build(); + MockServerWebExchange webExchange = MockServerWebExchange.from(request); + WebSessionManager webSessionManager = context.getBean(WebSessionManager.class); + WebSession webSession = webSessionManager.getSession(webExchange).block(); + session.accept(webSession); + }; + } + private Map getHandlerMap(ApplicationContext context) { HandlerMapping mapping = context.getBean("resourceHandlerMapping", HandlerMapping.class); if (mapping instanceof SimpleUrlHandlerMapping) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizerTests.java index f48db359c6..6ff4a99028 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizerTests.java @@ -122,7 +122,6 @@ class ServletWebServerFactoryCustomizerTests { assertThat(cookie.getComment()).isEqualTo("testcomment"); assertThat(cookie.getHttpOnly()).isTrue(); assertThat(cookie.getMaxAge()).hasSeconds(60); - } @Test 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 new file mode 100644 index 0000000000..cb47e7f33e --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Cookie.java @@ -0,0 +1,164 @@ +/* + * 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.server; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; + +import org.springframework.boot.convert.DurationUnit; + +/** + * Cookie properties. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @author Brian Clozel + * @author Weix Sun + * @since 2.6.0 + */ +public class Cookie { + + /** + * Name for the cookie. + */ + private String name; + + /** + * Domain for the cookie. + */ + private String domain; + + /** + * Path of the cookie. + */ + private String path; + + /** + * Whether to use "HttpOnly" cookies for the cookie. + */ + private Boolean httpOnly; + + /** + * Whether to always mark the cookie as secure. + */ + private Boolean secure; + + /** + * 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 + * time. A value of 0 means the cookie should expire immediately. A negative value + * means no "Max-Age". + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration maxAge; + + /** + * SameSite setting for the cookie. + */ + private SameSite sameSite; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDomain() { + return this.domain; + } + + public void setDomain(String domain) { + this.domain = domain; + } + + public String getPath() { + return this.path; + } + + public void setPath(String path) { + this.path = path; + } + + public Boolean getHttpOnly() { + return this.httpOnly; + } + + public void setHttpOnly(Boolean httpOnly) { + this.httpOnly = httpOnly; + } + + public Boolean getSecure() { + return this.secure; + } + + public void setSecure(Boolean secure) { + this.secure = secure; + } + + public Duration getMaxAge() { + return this.maxAge; + } + + public void setMaxAge(Duration maxAge) { + this.maxAge = maxAge; + } + + public SameSite getSameSite() { + return this.sameSite; + } + + public void setSameSite(SameSite sameSite) { + this.sameSite = sameSite; + } + + /** + * SameSite values. + */ + public enum SameSite { + + /** + * Cookies are sent in both first-party and cross-origin requests. + */ + NONE("None"), + + /** + * Cookies are sent in a first-party context, also when following a link to the + * origin site. + */ + LAX("Lax"), + + /** + * Cookies are only sent in a first-party context (i.e. not when following a link + * to the origin site). + */ + STRICT("Strict"); + + private final String attributeValue; + + SameSite(String attributeValue) { + this.attributeValue = attributeValue; + } + + public String attributeValue() { + return this.attributeValue; + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/Session.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/Session.java index 367072cb68..125bd98e13 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/Session.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/Session.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 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. @@ -47,10 +47,6 @@ public class Session { private final SessionStoreDirectory sessionStoreDirectory = new SessionStoreDirectory(); - public Cookie getCookie() { - return this.cookie; - } - public Duration getTimeout() { return this.timeout; } @@ -96,66 +92,24 @@ public class Session { this.storeDir = storeDir; } + public Cookie getCookie() { + return this.cookie; + } + SessionStoreDirectory getSessionStoreDirectory() { return this.sessionStoreDirectory; } /** - * Cookie properties. + * Session cookie properties. */ - public static class Cookie { - - private String name; - - private String domain; - - private String path; + public static class Cookie extends org.springframework.boot.web.server.Cookie { + /** + * Comment for the session cookie. + */ private String comment; - private Boolean httpOnly; - - private Boolean secure; - - @DurationUnit(ChronoUnit.SECONDS) - private Duration maxAge; - - /** - * Return the session cookie name. - * @return the session cookie name - */ - public String getName() { - return this.name; - } - - public void setName(String name) { - this.name = name; - } - - /** - * Return the domain for the session cookie. - * @return the session cookie domain - */ - public String getDomain() { - return this.domain; - } - - public void setDomain(String domain) { - this.domain = domain; - } - - /** - * Return the path of the session cookie. - * @return the session cookie path - */ - public String getPath() { - return this.path; - } - - public void setPath(String path) { - this.path = path; - } - /** * Return the comment for the session cookie. * @return the session cookie comment @@ -168,43 +122,6 @@ public class Session { this.comment = comment; } - /** - * Return whether to use "HttpOnly" cookies for session cookies. - * @return {@code true} to use "HttpOnly" cookies for session cookies. - */ - public Boolean getHttpOnly() { - return this.httpOnly; - } - - public void setHttpOnly(Boolean httpOnly) { - this.httpOnly = httpOnly; - } - - /** - * Return whether to always mark the session cookie as secure. - * @return {@code true} to mark the session cookie as secure even if the request - * that initiated the corresponding session is using plain HTTP - */ - public Boolean getSecure() { - return this.secure; - } - - public void setSecure(Boolean secure) { - this.secure = secure; - } - - /** - * Return the maximum age of the session cookie. - * @return the maximum age of the session cookie - */ - public Duration getMaxAge() { - return this.maxAge; - } - - public void setMaxAge(Duration maxAge) { - this.maxAge = maxAge; - } - } /**