From a2a802a14a6d3d75010c54fef50d6d79e8eb4fef Mon Sep 17 00:00:00 2001 From: weixsun Date: Mon, 24 May 2021 18:24:10 +0800 Subject: [PATCH 1/3] Add more session properties for reactive web servers Expand the session properties supported by reactive web servers to include `timeout` support and additional `cookie` properties. See gh-26714 --- .../MongoReactiveSessionConfiguration.java | 8 +- .../RedisReactiveSessionConfiguration.java | 8 +- .../session/SessionAutoConfiguration.java | 31 ++++- .../reactive/WebFluxAutoConfiguration.java | 43 +++++- .../web/reactive/WebFluxProperties.java | 128 ++++++++++++++++++ ...itional-spring-configuration-metadata.json | 20 +++ ...AbstractSessionAutoConfigurationTests.java | 35 ++++- ...iveSessionAutoConfigurationMongoTests.java | 43 ++++++ ...iveSessionAutoConfigurationRedisTests.java | 52 ++++++- .../WebFluxAutoConfigurationTests.java | 41 +++++- 10 files changed, 394 insertions(+), 15 deletions(-) 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..b70e8af20b 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.reactive.WebFluxProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; @@ -47,8 +48,9 @@ class MongoReactiveSessionConfiguration { static class SpringBootReactiveMongoWebSessionConfiguration extends ReactiveMongoWebSessionConfiguration { @Autowired - void customize(SessionProperties sessionProperties, MongoSessionProperties mongoSessionProperties) { - Duration timeout = sessionProperties.getTimeout(); + void customize(SessionProperties sessionProperties, MongoSessionProperties mongoSessionProperties, + WebFluxProperties webFluxProperties) { + Duration timeout = sessionProperties.determineTimeout(() -> webFluxProperties.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..b4a0b0b2ed 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.reactive.WebFluxProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; @@ -47,8 +48,9 @@ class RedisReactiveSessionConfiguration { static class SpringBootRedisWebSessionConfiguration extends RedisWebSessionConfiguration { @Autowired - void customize(SessionProperties sessionProperties, RedisSessionProperties redisSessionProperties) { - Duration timeout = sessionProperties.getTimeout(); + void customize(SessionProperties sessionProperties, RedisSessionProperties redisSessionProperties, + WebFluxProperties webFluxProperties) { + Duration timeout = sessionProperties.determineTimeout(() -> webFluxProperties.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..d93e64eb27 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 @@ -31,6 +31,7 @@ import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; 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.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration; @@ -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.WebFluxProperties.SameSite; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.web.servlet.server.Session.Cookie; @@ -62,6 +65,8 @@ import org.springframework.session.web.http.CookieHttpSessionIdResolver; import org.springframework.session.web.http.CookieSerializer; import org.springframework.session.web.http.DefaultCookieSerializer; import org.springframework.session.web.http.HttpSessionIdResolver; +import org.springframework.web.server.session.CookieWebSessionIdResolver; +import org.springframework.web.server.session.WebSessionIdResolver; /** * {@link EnableAutoConfiguration Auto-configuration} for Spring Session. @@ -71,12 +76,13 @@ 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 }) @@ -132,6 +138,29 @@ public class SessionAutoConfiguration { @Import(ReactiveSessionRepositoryValidator.class) static class ReactiveSessionConfiguration { + private static final String WEB_SESSION_ID_RESOLVER_BEAN_NAME = "webSessionIdResolver"; + + @Bean + @ConditionalOnMissingClass(WEB_SESSION_ID_RESOLVER_BEAN_NAME) + WebSessionIdResolver webSessionIdResolver(WebFluxProperties webFluxProperties) { + final WebFluxProperties.Cookie cookie = webFluxProperties.getSession().getCookie(); + CookieWebSessionIdResolver webSessionIdResolver = new CookieWebSessionIdResolver(); + webSessionIdResolver.setCookieName(cookie.getName()); + webSessionIdResolver.setCookieMaxAge(cookie.getMaxAge()); + webSessionIdResolver.addCookieInitializer((cookieBuilder) -> applyOtherProperties(cookie, cookieBuilder)); + return webSessionIdResolver; + } + + private void applyOtherProperties(WebFluxProperties.Cookie cookie, + org.springframework.http.ResponseCookie.ResponseCookieBuilder cookieBuilder) { + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(cookie::getDomain).to(cookieBuilder::domain); + map.from(cookie::getPath).to(cookieBuilder::path); + map.from(cookie::getHttpOnly).to(cookieBuilder::httpOnly); + map.from(cookie::getSecure).to(cookieBuilder::secure); + map.from(cookie::getSameSite).as(SameSite::attribute).to(cookieBuilder::sameSite); + } + @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(ReactiveSessionRepository.class) @Import({ ReactiveSessionRepositoryImplementationValidator.class, 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..c2742cea1c 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 @@ -21,6 +21,7 @@ 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; @@ -40,8 +41,11 @@ import org.springframework.boot.autoconfigure.web.WebProperties; import org.springframework.boot.autoconfigure.web.WebProperties.Resources; import org.springframework.boot.autoconfigure.web.format.DateTimeFormatters; import org.springframework.boot.autoconfigure.web.format.WebConversionService; +import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties.Cookie; import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties.Format; +import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties.SameSite; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.convert.ApplicationConversionService; import org.springframework.boot.web.codec.CodecCustomizer; import org.springframework.boot.web.reactive.filter.OrderedHiddenHttpMethodFilter; @@ -72,12 +76,14 @@ 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 +98,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) @@ -304,6 +311,9 @@ public class WebFluxAutoConfiguration { @ConditionalOnMissingBean(name = WebHttpHandlerBuilder.WEB_SESSION_MANAGER_BEAN_NAME) public WebSessionManager webSessionManager(ObjectProvider webSessionIdResolver) { DefaultWebSessionManager webSessionManager = new DefaultWebSessionManager(); + DefaultInMemoryWebSessionStore sessionStore = new DefaultInMemoryWebSessionStore( + this.webFluxProperties.getSession().getTimeout()); + webSessionManager.setSessionStore(sessionStore); webSessionManager.setSessionIdResolver(webSessionIdResolver.getIfAvailable(cookieWebSessionIdResolver())); return webSessionManager; } @@ -311,12 +321,41 @@ public class WebFluxAutoConfiguration { private Supplier cookieWebSessionIdResolver() { return () -> { CookieWebSessionIdResolver webSessionIdResolver = new CookieWebSessionIdResolver(); - webSessionIdResolver.addCookieInitializer((cookie) -> cookie - .sameSite(this.webFluxProperties.getSession().getCookie().getSameSite().attribute())); + webSessionIdResolver.setCookieName(this.webFluxProperties.getSession().getCookie().getName()); + webSessionIdResolver.addCookieInitializer((cookie) -> applyOtherProperties(cookie)); return webSessionIdResolver; }; } + private void applyOtherProperties(org.springframework.http.ResponseCookie.ResponseCookieBuilder cookieBuilder) { + Cookie cookie = this.webFluxProperties.getSession().getCookie(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(cookie::getDomain).to(cookieBuilder::domain); + map.from(cookie::getPath).to(cookieBuilder::path); + map.from(cookie::getMaxAge).to(cookieBuilder::maxAge); + map.from(cookie::getHttpOnly).to(cookieBuilder::httpOnly); + map.from(cookie::getSecure).to(cookieBuilder::secure); + map.from(cookie::getSameSite).as(SameSite::attribute).to(cookieBuilder::sameSite); + } + + static final class DefaultInMemoryWebSessionStore extends InMemoryWebSessionStore { + + private final Duration timeout; + + private DefaultInMemoryWebSessionStore(Duration timeout) { + this.timeout = timeout; + } + + @Override + public Mono createWebSession() { + return super.createWebSession().flatMap((inMemoryWebSession) -> { + inMemoryWebSession.setMaxIdleTime(this.timeout); + return Mono.just(inMemoryWebSession); + }); + } + + } + } @Configuration(proxyBeanMethods = false) 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..cea5999425 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 @@ -16,7 +16,11 @@ package org.springframework.boot.autoconfigure.web.reactive; +import java.time.Duration; +import java.time.temporal.ChronoUnit; + import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.convert.DurationUnit; import org.springframework.util.StringUtils; /** @@ -124,21 +128,145 @@ public class WebFluxProperties { 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 Cookie getCookie() { return this.cookie; } + public Duration getTimeout() { + return this.timeout; + } + + public void setTimeout(Duration timeout) { + this.timeout = timeout; + } + } public static class Cookie { + private static final String COOKIE_NAME = "SESSION"; + + /** + * Name attribute value for session Cookies. + */ + private String name = COOKIE_NAME; + + /** + * Domain attribute value for session Cookies. + */ + private String domain; + + /** + * Path attribute value for session Cookies. + */ + private String path; + + /** + * Maximum age of the session 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" attribute in which case the + * cookie is removed when the browser is closed. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration maxAge = Duration.ofSeconds(-1); + + /** + * HttpOnly attribute value for session Cookies. + */ + private Boolean httpOnly = true; + + /** + * Secure attribute value for session Cookies. + */ + private Boolean secure; + /** * SameSite attribute value for session Cookies. */ private SameSite sameSite = SameSite.LAX; + /** + * 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 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; + } + + /** + * 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; + } + public SameSite getSameSite() { return this.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 607d04177e..13a9319471 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 @@ -2027,6 +2027,26 @@ "description": "Whether to enable Spring's HiddenHttpMethodFilter.", "defaultValue": false }, + { + "name": "spring.webflux.session.timeout", + "defaultValue": "30m" + }, + { + "name": "spring.webflux.session.cookie.name", + "defaultValue": "SESSION" + }, + { + "name": "spring.webflux.session.cookie.path", + "defaultValue": "server.servlet.context-path" + }, + { + "name": "spring.webflux.session.cookie.max-age", + "defaultValue": "-1s" + }, + { + "name": "spring.webflux.session.cookie.http-only", + "defaultValue": true + }, { "name": "spring.webflux.session.cookie.same-site", "defaultValue": "lax" 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..26904484b5 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; @@ -28,6 +31,7 @@ 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 +41,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 +80,19 @@ class ReactiveSessionAutoConfigurationMongoTests extends AbstractSessionAutoConf }); } + @Test + void defaultConfigWithCustomWebFluxTimeout() { + this.contextRunner.withPropertyValues("spring.session.store-type=mongodb", "spring.webflux.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 +103,31 @@ class ReactiveSessionAutoConfigurationMongoTests extends AbstractSessionAutoConf .run(validateSpringSessionUsesMongo("foo")); } + @Test + void sessionCookieConfigurationIsAppliedToAutoConfiguredWebSessionIdResolver() { + this.contextRunner + .withConfiguration(AutoConfigurations.of(EmbeddedMongoAutoConfiguration.class, + MongoAutoConfiguration.class, MongoDataAutoConfiguration.class, + MongoReactiveAutoConfiguration.class, MongoReactiveDataAutoConfiguration.class)) + .withUserConfiguration(Config.class) + .withPropertyValues("spring.session.store-type=mongodb", + "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 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..201dee31f8 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; @@ -56,6 +57,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; @@ -568,10 +570,30 @@ class WebFluxAutoConfigurationTests { } @Test - void customSameSiteConfigurationShouldBeApplied() { - 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")))); + void customSessionTimeoutConfigurationShouldBeApplied() { + this.contextRunner.withPropertyValues("spring.webflux.session.timeout:123") + .run((assertSessionTimeoutWithWebSession((webSession) -> { + webSession.start(); + assertThat(webSession.getMaxIdleTime()).hasSeconds(123); + }))); + } + + @Test + void customSessionCookieConfigurationShouldBeApplied() { + this.contextRunner.withPropertyValues("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 assertExchangeWithSession( @@ -587,6 +609,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) { From 3c71637fa2204d0b5f28227f122886f9835dd113 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 19 Oct 2021 20:41:02 -0700 Subject: [PATCH 2/3] Polish 'Add more session properties for reactive web servers' See gh-26714 --- .../MongoReactiveSessionConfiguration.java | 1 + .../RedisReactiveSessionConfiguration.java | 1 + .../session/SessionAutoConfiguration.java | 41 ++++++---- .../reactive/WebFluxAutoConfiguration.java | 45 ++++++----- .../web/reactive/WebFluxProperties.java | 75 ++++++------------- ...ervletWebServerFactoryCustomizerTests.java | 1 - .../boot/web/servlet/server/Session.java | 10 +-- 7 files changed, 81 insertions(+), 93 deletions(-) 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 b70e8af20b..41f9464e7c 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 @@ -35,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 }) 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 b4a0b0b2ed..23d9c7d39e 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 @@ -35,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 }) 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 d93e64eb27..b50810e7ed 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; @@ -31,7 +32,6 @@ import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; 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.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration; @@ -56,6 +56,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.ImportSelector; import org.springframework.core.type.AnnotationMetadata; +import org.springframework.http.ResponseCookie.ResponseCookieBuilder; import org.springframework.security.web.authentication.RememberMeServices; import org.springframework.session.ReactiveSessionRepository; import org.springframework.session.Session; @@ -65,6 +66,7 @@ import org.springframework.session.web.http.CookieHttpSessionIdResolver; import org.springframework.session.web.http.CookieSerializer; import org.springframework.session.web.http.DefaultCookieSerializer; import org.springframework.session.web.http.HttpSessionIdResolver; +import org.springframework.util.StringUtils; import org.springframework.web.server.session.CookieWebSessionIdResolver; import org.springframework.web.server.session.WebSessionIdResolver; @@ -106,7 +108,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; } @@ -138,27 +140,34 @@ public class SessionAutoConfiguration { @Import(ReactiveSessionRepositoryValidator.class) static class ReactiveSessionConfiguration { - private static final String WEB_SESSION_ID_RESOLVER_BEAN_NAME = "webSessionIdResolver"; + private final WebFluxProperties webFluxProperties; + + ReactiveSessionConfiguration(WebFluxProperties webFluxProperties) { + this.webFluxProperties = webFluxProperties; + } @Bean - @ConditionalOnMissingClass(WEB_SESSION_ID_RESOLVER_BEAN_NAME) - WebSessionIdResolver webSessionIdResolver(WebFluxProperties webFluxProperties) { - final WebFluxProperties.Cookie cookie = webFluxProperties.getSession().getCookie(); + @ConditionalOnMissingBean + WebSessionIdResolver webSessionIdResolver() { + WebFluxProperties.Cookie cookieProperties = this.webFluxProperties.getSession().getCookie(); CookieWebSessionIdResolver webSessionIdResolver = new CookieWebSessionIdResolver(); - webSessionIdResolver.setCookieName(cookie.getName()); - webSessionIdResolver.setCookieMaxAge(cookie.getMaxAge()); - webSessionIdResolver.addCookieInitializer((cookieBuilder) -> applyOtherProperties(cookie, cookieBuilder)); + String cookieName = cookieProperties.getName(); + if (StringUtils.hasText(cookieName)) { + webSessionIdResolver.setCookieName(cookieName); + } + webSessionIdResolver.addCookieInitializer(this::initializeCookie); return webSessionIdResolver; } - private void applyOtherProperties(WebFluxProperties.Cookie cookie, - org.springframework.http.ResponseCookie.ResponseCookieBuilder cookieBuilder) { + private void initializeCookie(ResponseCookieBuilder builder) { + WebFluxProperties.Cookie cookie = this.webFluxProperties.getSession().getCookie(); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - map.from(cookie::getDomain).to(cookieBuilder::domain); - map.from(cookie::getPath).to(cookieBuilder::path); - map.from(cookie::getHttpOnly).to(cookieBuilder::httpOnly); - map.from(cookie::getSecure).to(cookieBuilder::secure); - map.from(cookie::getSameSite).as(SameSite::attribute).to(cookieBuilder::sameSite); + 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(cookie::getSameSite).as(SameSite::attribute).to(builder::sameSite); } @Configuration(proxyBeanMethods = false) 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 c2742cea1c..d5949fafde 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 @@ -57,8 +57,10 @@ import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.format.FormatterRegistry; import org.springframework.format.support.FormattingConversionService; +import org.springframework.http.ResponseCookie.ResponseCookieBuilder; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; import org.springframework.validation.Validator; import org.springframework.web.filter.reactive.HiddenHttpMethodFilter; import org.springframework.web.reactive.config.DelegatingWebFluxConfiguration; @@ -311,47 +313,50 @@ public class WebFluxAutoConfiguration { @ConditionalOnMissingBean(name = WebHttpHandlerBuilder.WEB_SESSION_MANAGER_BEAN_NAME) public WebSessionManager webSessionManager(ObjectProvider webSessionIdResolver) { DefaultWebSessionManager webSessionManager = new DefaultWebSessionManager(); - DefaultInMemoryWebSessionStore sessionStore = new DefaultInMemoryWebSessionStore( - this.webFluxProperties.getSession().getTimeout()); - webSessionManager.setSessionStore(sessionStore); + Duration timeout = this.webFluxProperties.getSession().getTimeout(); + webSessionManager.setSessionStore(new MaxIdleTimeInMemoryWebSessionStore(timeout)); webSessionManager.setSessionIdResolver(webSessionIdResolver.getIfAvailable(cookieWebSessionIdResolver())); return webSessionManager; } private Supplier cookieWebSessionIdResolver() { return () -> { - CookieWebSessionIdResolver webSessionIdResolver = new CookieWebSessionIdResolver(); - webSessionIdResolver.setCookieName(this.webFluxProperties.getSession().getCookie().getName()); - webSessionIdResolver.addCookieInitializer((cookie) -> applyOtherProperties(cookie)); - return webSessionIdResolver; + CookieWebSessionIdResolver resolver = new CookieWebSessionIdResolver(); + String cookieName = this.webFluxProperties.getSession().getCookie().getName(); + if (StringUtils.hasText(cookieName)) { + resolver.setCookieName(cookieName); + } + resolver.addCookieInitializer(this::initializeCookie); + return resolver; }; } - private void applyOtherProperties(org.springframework.http.ResponseCookie.ResponseCookieBuilder cookieBuilder) { + private void initializeCookie(ResponseCookieBuilder builder) { Cookie cookie = this.webFluxProperties.getSession().getCookie(); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - map.from(cookie::getDomain).to(cookieBuilder::domain); - map.from(cookie::getPath).to(cookieBuilder::path); - map.from(cookie::getMaxAge).to(cookieBuilder::maxAge); - map.from(cookie::getHttpOnly).to(cookieBuilder::httpOnly); - map.from(cookie::getSecure).to(cookieBuilder::secure); - map.from(cookie::getSameSite).as(SameSite::attribute).to(cookieBuilder::sameSite); + 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(cookie::getSameSite).as(SameSite::attribute).to(builder::sameSite); } - static final class DefaultInMemoryWebSessionStore extends InMemoryWebSessionStore { + static final class MaxIdleTimeInMemoryWebSessionStore extends InMemoryWebSessionStore { private final Duration timeout; - private DefaultInMemoryWebSessionStore(Duration timeout) { + private MaxIdleTimeInMemoryWebSessionStore(Duration timeout) { this.timeout = timeout; } @Override public Mono createWebSession() { - return super.createWebSession().flatMap((inMemoryWebSession) -> { - inMemoryWebSession.setMaxIdleTime(this.timeout); - return Mono.just(inMemoryWebSession); - }); + 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 cea5999425..e47c47503e 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 @@ -136,10 +136,6 @@ public class WebFluxProperties { private final Cookie cookie = new Cookie(); - public Cookie getCookie() { - return this.cookie; - } - public Duration getTimeout() { return this.timeout; } @@ -148,16 +144,18 @@ public class WebFluxProperties { this.timeout = timeout; } + public Cookie getCookie() { + return this.cookie; + } + } public static class Cookie { - private static final String COOKIE_NAME = "SESSION"; - /** * Name attribute value for session Cookies. */ - private String name = COOKIE_NAME; + private String name; /** * Domain attribute value for session Cookies. @@ -169,20 +167,10 @@ public class WebFluxProperties { */ private String path; - /** - * Maximum age of the session 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" attribute in which case the - * cookie is removed when the browser is closed. - */ - @DurationUnit(ChronoUnit.SECONDS) - private Duration maxAge = Duration.ofSeconds(-1); - /** * HttpOnly attribute value for session Cookies. */ - private Boolean httpOnly = true; + private Boolean httpOnly; /** * Secure attribute value for session Cookies. @@ -190,14 +178,20 @@ public class WebFluxProperties { private Boolean secure; /** - * SameSite attribute value for session Cookies. + * Maximum age of the session 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" attribute in which case the + * cookie is removed when the browser is closed. */ - private SameSite sameSite = SameSite.LAX; + @DurationUnit(ChronoUnit.SECONDS) + private Duration maxAge; /** - * Return the session cookie name. - * @return the session cookie name + * SameSite attribute value for session Cookies. */ + private SameSite sameSite; + public String getName() { return this.name; } @@ -206,10 +200,6 @@ public class WebFluxProperties { this.name = name; } - /** - * Return the domain for the session cookie. - * @return the session cookie domain - */ public String getDomain() { return this.domain; } @@ -218,10 +208,6 @@ public class WebFluxProperties { this.domain = domain; } - /** - * Return the path of the session cookie. - * @return the session cookie path - */ public String getPath() { return this.path; } @@ -230,22 +216,6 @@ public class WebFluxProperties { this.path = path; } - /** - * 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; - } - - /** - * 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; } @@ -254,11 +224,6 @@ public class WebFluxProperties { 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; } @@ -267,6 +232,14 @@ public class WebFluxProperties { this.secure = secure; } + public Duration getMaxAge() { + return this.maxAge; + } + + public void setMaxAge(Duration maxAge) { + this.maxAge = maxAge; + } + public SameSite getSameSite() { return this.sameSite; } 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/servlet/server/Session.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/Session.java index 367072cb68..86432feed8 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,6 +92,10 @@ public class Session { this.storeDir = storeDir; } + public Cookie getCookie() { + return this.cookie; + } + SessionStoreDirectory getSessionStoreDirectory() { return this.sessionStoreDirectory; } From b72ff2522004a3ad5d495fa4407869bed273f06a Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 19 Oct 2021 22:21:12 -0700 Subject: [PATCH 3/3] Relocate and unify reactive cookie properties Relocate the recently introduced `spring.webflux.session` properties to `server.reactive.session` and create a unified `Cookie` properties class. Reactive session properties now mirror the existing `server.servlet.session` properties and better reflect the fact that they are related to the server and not just for WebFlux. See gh-26714 --- .../MongoReactiveSessionConfiguration.java | 7 +- .../RedisReactiveSessionConfiguration.java | 7 +- .../session/SessionAutoConfiguration.java | 39 +---- .../autoconfigure/web/ServerProperties.java | 47 ++++- .../reactive/WebFluxAutoConfiguration.java | 83 ++++----- .../web/reactive/WebFluxProperties.java | 125 +++---------- ...WebSessionIdResolverAutoConfiguration.java | 106 +++++++++++ ...itional-spring-configuration-metadata.json | 48 ----- .../main/resources/META-INF/spring.factories | 1 + ...iveSessionAutoConfigurationMongoTests.java | 24 +-- .../WebFluxAutoConfigurationTests.java | 31 +++- .../boot/web/server/Cookie.java | 164 ++++++++++++++++++ .../boot/web/servlet/server/Session.java | 93 +--------- 13 files changed, 423 insertions(+), 352 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebSessionIdResolverAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Cookie.java 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 41f9464e7c..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 @@ -22,7 +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.reactive.WebFluxProperties; +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; @@ -50,8 +50,9 @@ class MongoReactiveSessionConfiguration { @Autowired void customize(SessionProperties sessionProperties, MongoSessionProperties mongoSessionProperties, - WebFluxProperties webFluxProperties) { - Duration timeout = sessionProperties.determineTimeout(() -> webFluxProperties.getSession().getTimeout()); + 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 23d9c7d39e..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 @@ -22,7 +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.reactive.WebFluxProperties; +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; @@ -50,8 +50,9 @@ class RedisReactiveSessionConfiguration { @Autowired void customize(SessionProperties sessionProperties, RedisSessionProperties redisSessionProperties, - WebFluxProperties webFluxProperties) { - Duration timeout = sessionProperties.determineTimeout(() -> webFluxProperties.getSession().getTimeout()); + 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 b50810e7ed..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 @@ -45,7 +45,7 @@ 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.WebFluxProperties.SameSite; +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; @@ -56,7 +56,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.ImportSelector; import org.springframework.core.type.AnnotationMetadata; -import org.springframework.http.ResponseCookie.ResponseCookieBuilder; import org.springframework.security.web.authentication.RememberMeServices; import org.springframework.session.ReactiveSessionRepository; import org.springframework.session.Session; @@ -66,9 +65,6 @@ import org.springframework.session.web.http.CookieHttpSessionIdResolver; import org.springframework.session.web.http.CookieSerializer; import org.springframework.session.web.http.DefaultCookieSerializer; import org.springframework.session.web.http.HttpSessionIdResolver; -import org.springframework.util.StringUtils; -import org.springframework.web.server.session.CookieWebSessionIdResolver; -import org.springframework.web.server.session.WebSessionIdResolver; /** * {@link EnableAutoConfiguration Auto-configuration} for Spring Session. @@ -87,7 +83,8 @@ import org.springframework.web.server.session.WebSessionIdResolver; @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 { @@ -140,36 +137,6 @@ public class SessionAutoConfiguration { @Import(ReactiveSessionRepositoryValidator.class) static class ReactiveSessionConfiguration { - private final WebFluxProperties webFluxProperties; - - ReactiveSessionConfiguration(WebFluxProperties webFluxProperties) { - this.webFluxProperties = webFluxProperties; - } - - @Bean - @ConditionalOnMissingBean - WebSessionIdResolver webSessionIdResolver() { - WebFluxProperties.Cookie cookieProperties = this.webFluxProperties.getSession().getCookie(); - CookieWebSessionIdResolver webSessionIdResolver = new CookieWebSessionIdResolver(); - String cookieName = cookieProperties.getName(); - if (StringUtils.hasText(cookieName)) { - webSessionIdResolver.setCookieName(cookieName); - } - webSessionIdResolver.addCookieInitializer(this::initializeCookie); - return webSessionIdResolver; - } - - private void initializeCookie(ResponseCookieBuilder builder) { - WebFluxProperties.Cookie cookie = this.webFluxProperties.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(cookie::getSameSite).as(SameSite::attribute).to(builder::sameSite); - } - @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(ReactiveSessionRepository.class) @Import({ ReactiveSessionRepositoryImplementationValidator.class, 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 d5949fafde..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,7 +17,6 @@ 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; @@ -37,15 +36,13 @@ 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; import org.springframework.boot.autoconfigure.web.format.WebConversionService; -import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties.Cookie; import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties.Format; -import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties.SameSite; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.convert.ApplicationConversionService; import org.springframework.boot.web.codec.CodecCustomizer; import org.springframework.boot.web.reactive.filter.OrderedHiddenHttpMethodFilter; @@ -57,10 +54,8 @@ import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.format.FormatterRegistry; import org.springframework.format.support.FormattingConversionService; -import org.springframework.http.ResponseCookie.ResponseCookieBuilder; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.util.ClassUtils; -import org.springframework.util.StringUtils; import org.springframework.validation.Validator; import org.springframework.web.filter.reactive.HiddenHttpMethodFilter; import org.springframework.web.reactive.config.DelegatingWebFluxConfiguration; @@ -83,7 +78,6 @@ 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; @@ -108,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 { @@ -240,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(); } @@ -313,54 +311,12 @@ public class WebFluxAutoConfiguration { @ConditionalOnMissingBean(name = WebHttpHandlerBuilder.WEB_SESSION_MANAGER_BEAN_NAME) public WebSessionManager webSessionManager(ObjectProvider webSessionIdResolver) { DefaultWebSessionManager webSessionManager = new DefaultWebSessionManager(); - Duration timeout = this.webFluxProperties.getSession().getTimeout(); + Duration timeout = this.serverProperties.getReactive().getSession().getTimeout(); webSessionManager.setSessionStore(new MaxIdleTimeInMemoryWebSessionStore(timeout)); - webSessionManager.setSessionIdResolver(webSessionIdResolver.getIfAvailable(cookieWebSessionIdResolver())); + webSessionIdResolver.ifAvailable(webSessionManager::setSessionIdResolver); return webSessionManager; } - private Supplier cookieWebSessionIdResolver() { - return () -> { - CookieWebSessionIdResolver resolver = new CookieWebSessionIdResolver(); - String cookieName = this.webFluxProperties.getSession().getCookie().getName(); - if (StringUtils.hasText(cookieName)) { - resolver.setCookieName(cookieName); - } - resolver.addCookieInitializer(this::initializeCookie); - return resolver; - }; - } - - private void initializeCookie(ResponseCookieBuilder builder) { - Cookie cookie = this.webFluxProperties.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(cookie::getSameSite).as(SameSite::attribute).to(builder::sameSite); - } - - 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); - } - - } - } @Configuration(proxyBeanMethods = false) @@ -375,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 e47c47503e..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 @@ -16,11 +16,8 @@ package org.springframework.boot.autoconfigure.web.reactive; -import java.time.Duration; -import java.time.temporal.ChronoUnit; - import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.convert.DurationUnit; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; import org.springframework.util.StringUtils; /** @@ -71,6 +68,7 @@ public class WebFluxProperties { return this.format; } + @DeprecatedConfigurationProperty(replacement = "server.reactive.session") public Session getSession() { return this.session; } @@ -126,120 +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 { - /** - * 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; - } - + @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 { - /** - * Name attribute value for session Cookies. - */ - private String name; - - /** - * Domain attribute value for session Cookies. - */ - private String domain; - - /** - * Path attribute value for session Cookies. - */ - private String path; - - /** - * HttpOnly attribute value for session Cookies. - */ - private Boolean httpOnly; - - /** - * Secure attribute value for session Cookies. - */ - private Boolean secure; - - /** - * Maximum age of the session 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" attribute in which case the - * cookie is removed when the browser is closed. - */ - @DurationUnit(ChronoUnit.SECONDS) - private Duration maxAge; - /** * SameSite attribute value for session Cookies. */ 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; - } - + @DeprecatedConfigurationProperty(replacement = "server.reactive.session.cookie.same-site") public SameSite getSameSite() { return this.sameSite; } @@ -250,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 13a9319471..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.", @@ -2031,26 +2003,6 @@ "name": "spring.webflux.session.timeout", "defaultValue": "30m" }, - { - "name": "spring.webflux.session.cookie.name", - "defaultValue": "SESSION" - }, - { - "name": "spring.webflux.session.cookie.path", - "defaultValue": "server.servlet.context-path" - }, - { - "name": "spring.webflux.session.cookie.max-age", - "defaultValue": "-1s" - }, - { - "name": "spring.webflux.session.cookie.http-only", - "defaultValue": true - }, - { - "name": "spring.webflux.session.cookie.same-site", - "defaultValue": "lax" - }, { "name": "spring.webservices.wsdl-locations", "type": "java.util.List", 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/ReactiveSessionAutoConfigurationMongoTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/ReactiveSessionAutoConfigurationMongoTests.java index 26904484b5..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 @@ -27,6 +27,7 @@ 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; @@ -81,8 +82,8 @@ class ReactiveSessionAutoConfigurationMongoTests extends AbstractSessionAutoConf } @Test - void defaultConfigWithCustomWebFluxTimeout() { - this.contextRunner.withPropertyValues("spring.session.store-type=mongodb", "spring.webflux.session.timeout=1m") + 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)) @@ -105,17 +106,16 @@ class ReactiveSessionAutoConfigurationMongoTests extends AbstractSessionAutoConf @Test void sessionCookieConfigurationIsAppliedToAutoConfiguredWebSessionIdResolver() { - this.contextRunner - .withConfiguration(AutoConfigurations.of(EmbeddedMongoAutoConfiguration.class, - MongoAutoConfiguration.class, MongoDataAutoConfiguration.class, - MongoReactiveAutoConfiguration.class, MongoReactiveDataAutoConfiguration.class)) - .withUserConfiguration(Config.class) + 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", - "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") + "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(); 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 201dee31f8..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 @@ -40,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; @@ -111,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 @@ -571,19 +573,36 @@ class WebFluxAutoConfigurationTests { @Test void customSessionTimeoutConfigurationShouldBeApplied() { - this.contextRunner.withPropertyValues("spring.webflux.session.timeout:123") + 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("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") + 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(); 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 86432feed8..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 @@ -101,61 +101,15 @@ public class Session { } /** - * 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; - } - } /**