From 3c71637fa2204d0b5f28227f122886f9835dd113 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 19 Oct 2021 20:41:02 -0700 Subject: [PATCH] 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; }