From a2a802a14a6d3d75010c54fef50d6d79e8eb4fef Mon Sep 17 00:00:00 2001 From: weixsun Date: Mon, 24 May 2021 18:24:10 +0800 Subject: [PATCH] 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 73ecbf0119f..b70e8af20b0 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 15edb73b3b8..b4a0b0b2edd 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 0c055004e9c..d93e64eb273 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 d9ce69ead68..c2742cea1c5 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 a04fd0aada7..cea59994255 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 607d04177e0..13a93194715 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 510beb70c1c..a01c6b534b4 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 ea63167f4a4..26904484b53 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 c85272dd151..89a04478d6c 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 59edb80ea7b..201dee31f87 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) {