mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-09-17 12:09:16 +00:00
Decouple Session auto-configuration from ServerProperties
Fixes gh-48493
This commit is contained in:
@@ -29,10 +29,9 @@ dependencies {
|
||||
api(project(":core:spring-boot"))
|
||||
api("org.springframework.session:spring-session-core")
|
||||
|
||||
implementation(project(":module:spring-boot-web-server"))
|
||||
|
||||
optional(project(":core:spring-boot-autoconfigure"))
|
||||
optional(project(":module:spring-boot-actuator-autoconfigure"))
|
||||
optional(project(":module:spring-boot-web-server"))
|
||||
optional("io.projectreactor:reactor-core")
|
||||
optional("jakarta.servlet:jakarta.servlet-api")
|
||||
optional("org.springframework.security:spring-security-web")
|
||||
|
||||
+99
-20
@@ -17,6 +17,11 @@
|
||||
package org.springframework.boot.session.autoconfigure;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import jakarta.servlet.ServletContext;
|
||||
import jakarta.servlet.SessionCookieConfig;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
@@ -25,6 +30,8 @@ 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.ConditionalOnNotWarDeployment;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWarDeployment;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
@@ -43,6 +50,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.util.Assert;
|
||||
import org.springframework.web.context.ServletContextAware;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for Spring Session.
|
||||
@@ -58,33 +67,19 @@ import org.springframework.session.web.http.HttpSessionIdResolver;
|
||||
@AutoConfiguration
|
||||
@ConditionalOnClass(Session.class)
|
||||
@ConditionalOnWebApplication
|
||||
@EnableConfigurationProperties({ ServerProperties.class, SessionProperties.class })
|
||||
@EnableConfigurationProperties(SessionProperties.class)
|
||||
public final class SessionAutoConfiguration {
|
||||
|
||||
private static Duration determineTimeout(SessionProperties sessionProperties, Supplier<Duration> fallbackTimeout) {
|
||||
Duration timeout = sessionProperties.getTimeout();
|
||||
return (timeout != null) ? timeout : fallbackTimeout.get();
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnWebApplication(type = Type.SERVLET)
|
||||
@Import(SessionRepositoryFilterConfiguration.class)
|
||||
static class ServletSessionConfiguration {
|
||||
|
||||
@Bean
|
||||
@Conditional(DefaultCookieSerializerCondition.class)
|
||||
DefaultCookieSerializer cookieSerializer(ServerProperties serverProperties,
|
||||
ObjectProvider<DefaultCookieSerializerCustomizer> cookieSerializerCustomizers) {
|
||||
Cookie cookie = serverProperties.getServlet().getSession().getCookie();
|
||||
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
|
||||
PropertyMapper map = PropertyMapper.get();
|
||||
map.from(cookie::getName).to(cookieSerializer::setCookieName);
|
||||
map.from(cookie::getDomain).to(cookieSerializer::setDomainName);
|
||||
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).asInt(Duration::getSeconds).to(cookieSerializer::setCookieMaxAge);
|
||||
map.from(cookie::getSameSite).as(SameSite::attributeValue).always().to(cookieSerializer::setSameSite);
|
||||
map.from(cookie::getPartitioned).to(cookieSerializer::setPartitioned);
|
||||
cookieSerializerCustomizers.orderedStream().forEach((customizer) -> customizer.customize(cookieSerializer));
|
||||
return cookieSerializer;
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(RememberMeServices.class)
|
||||
static class RememberMeServicesConfiguration {
|
||||
@@ -97,6 +92,90 @@ public final class SessionAutoConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@ConditionalOnNotWarDeployment
|
||||
@EnableConfigurationProperties(ServerProperties.class)
|
||||
static class EmbeddedWebServerConfiguration {
|
||||
|
||||
@Bean
|
||||
SessionTimeout embeddedWebServerSessionTimeout(SessionProperties sessionProperties,
|
||||
ServerProperties serverProperties) {
|
||||
return () -> determineTimeout(sessionProperties,
|
||||
serverProperties.getServlet().getSession()::getTimeout);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Conditional(DefaultCookieSerializerCondition.class)
|
||||
DefaultCookieSerializer cookieSerializer(ServerProperties serverProperties,
|
||||
ObjectProvider<DefaultCookieSerializerCustomizer> cookieSerializerCustomizers) {
|
||||
Cookie cookie = serverProperties.getServlet().getSession().getCookie();
|
||||
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
|
||||
PropertyMapper map = PropertyMapper.get();
|
||||
map.from(cookie::getName).to(cookieSerializer::setCookieName);
|
||||
map.from(cookie::getDomain).to(cookieSerializer::setDomainName);
|
||||
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).asInt(Duration::getSeconds).to(cookieSerializer::setCookieMaxAge);
|
||||
map.from(cookie::getSameSite).as(SameSite::attributeValue).always().to(cookieSerializer::setSameSite);
|
||||
map.from(cookie::getPartitioned).to(cookieSerializer::setPartitioned);
|
||||
cookieSerializerCustomizers.orderedStream()
|
||||
.forEach((customizer) -> customizer.customize(cookieSerializer));
|
||||
return cookieSerializer;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ConditionalOnWarDeployment
|
||||
static class WarDepoymentConfiguration implements ServletContextAware {
|
||||
|
||||
private @Nullable ServletContext servletContext;
|
||||
|
||||
@Override
|
||||
public void setServletContext(ServletContext servletContext) {
|
||||
this.servletContext = servletContext;
|
||||
}
|
||||
|
||||
@Bean
|
||||
SessionTimeout warDeplomentSessionTimeout(SessionProperties sessionProperties) {
|
||||
return sessionProperties::getTimeout;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Conditional(DefaultCookieSerializerCondition.class)
|
||||
DefaultCookieSerializer cookieSerializer(
|
||||
ObjectProvider<DefaultCookieSerializerCustomizer> cookieSerializerCustomizers) {
|
||||
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
|
||||
PropertyMapper map = PropertyMapper.get();
|
||||
Assert.notNull(this.servletContext,
|
||||
"ServletContext is required for session configuration in a war deployment");
|
||||
SessionCookieConfig cookie = this.servletContext.getSessionCookieConfig();
|
||||
map.from(cookie::getName).to(cookieSerializer::setCookieName);
|
||||
map.from(cookie::getDomain).to(cookieSerializer::setDomainName);
|
||||
map.from(cookie::getPath).to(cookieSerializer::setCookiePath);
|
||||
map.from(cookie::isHttpOnly).to(cookieSerializer::setUseHttpOnlyCookie);
|
||||
map.from(cookie::isSecure).to(cookieSerializer::setUseSecureCookie);
|
||||
map.from(cookie::getMaxAge).to(cookieSerializer::setCookieMaxAge);
|
||||
map.from(cookie.getAttribute("SameSite")).always().to(cookieSerializer::setSameSite);
|
||||
map.from(cookie.getAttribute("Partitioned")).as(Boolean::valueOf).to(cookieSerializer::setPartitioned);
|
||||
cookieSerializerCustomizers.orderedStream()
|
||||
.forEach((customizer) -> customizer.customize(cookieSerializer));
|
||||
return cookieSerializer;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnWebApplication(type = Type.REACTIVE)
|
||||
static class ReactiveSessionConfiguration {
|
||||
|
||||
@Bean
|
||||
SessionTimeout embeddedWebServerSessionTimeout(SessionProperties sessionProperties,
|
||||
ServerProperties serverProperties) {
|
||||
return () -> determineTimeout(sessionProperties, serverProperties.getReactive().getSession()::getTimeout);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+2
@@ -70,7 +70,9 @@ public class SessionProperties {
|
||||
* {@code fallbackTimeout} is used.
|
||||
* @param fallbackTimeout a fallback timeout value if the timeout isn't configured
|
||||
* @return the session timeout
|
||||
* @deprecated since 4.0.1 for removal in 4.2.0 in favor of {@link SessionTimeout}
|
||||
*/
|
||||
@Deprecated(since = "4.0.1", forRemoval = true)
|
||||
public Duration determineTimeout(Supplier<Duration> fallbackTimeout) {
|
||||
return (this.timeout != null) ? this.timeout : fallbackTimeout.get();
|
||||
}
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.session.autoconfigure;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Provides access to the configured session timeout. The timeout is available
|
||||
* irrespective of the deployment type:
|
||||
* <ul>
|
||||
* <li>a servlet web application
|
||||
* <ul>
|
||||
* <li>using an embedded web server
|
||||
* <li>deployed as a war file
|
||||
* </ul>
|
||||
* <li>a reactive web application
|
||||
* </ul>
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 4.0.1
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface SessionTimeout {
|
||||
|
||||
/**
|
||||
* Returns the session timeout or {@code null} if no timeout has been configured.
|
||||
* @return the timeout or {@code null}
|
||||
*/
|
||||
@Nullable Duration getTimeout();
|
||||
|
||||
}
|
||||
+30
@@ -22,6 +22,7 @@ import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.SessionCookieConfig;
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InOrder;
|
||||
@@ -34,6 +35,7 @@ import org.springframework.boot.web.servlet.AbstractFilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.mock.web.MockServletContext;
|
||||
import org.springframework.session.MapSessionRepository;
|
||||
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
|
||||
import org.springframework.session.security.web.authentication.SpringSessionRememberMeServices;
|
||||
@@ -60,6 +62,7 @@ import static org.mockito.Mockito.mock;
|
||||
class SessionAutoConfigurationTests {
|
||||
|
||||
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
|
||||
.withInitializer((context) -> context.setServletContext(null)) // not a war
|
||||
.withConfiguration(AutoConfigurations.of(SessionAutoConfiguration.class));
|
||||
|
||||
@Test
|
||||
@@ -138,6 +141,33 @@ class SessionAutoConfigurationTests {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void sessionCookieConfigIsAppliedToAutoConfiguredCookieSerializerInAWarDeployment() {
|
||||
MockServletContext servletContext = new MockServletContext();
|
||||
SessionCookieConfig cookie = servletContext.getSessionCookieConfig();
|
||||
cookie.setName("sid");
|
||||
cookie.setDomain("spring");
|
||||
cookie.setPath("/test");
|
||||
cookie.setHttpOnly(false);
|
||||
cookie.setSecure(false);
|
||||
cookie.setMaxAge(10);
|
||||
cookie.setAttribute("SameSite", "Strict");
|
||||
cookie.setAttribute("Partitioned", "true");
|
||||
this.contextRunner.withUserConfiguration(SessionRepositoryConfiguration.class)
|
||||
.withInitializer((context) -> context.setServletContext(servletContext)) // war
|
||||
.run((context) -> {
|
||||
DefaultCookieSerializer cookieSerializer = context.getBean(DefaultCookieSerializer.class);
|
||||
assertThat(cookieSerializer).hasFieldOrPropertyWithValue("cookieName", "sid");
|
||||
assertThat(cookieSerializer).hasFieldOrPropertyWithValue("domainName", "spring");
|
||||
assertThat(cookieSerializer).hasFieldOrPropertyWithValue("cookiePath", "/test");
|
||||
assertThat(cookieSerializer).hasFieldOrPropertyWithValue("useHttpOnlyCookie", false);
|
||||
assertThat(cookieSerializer).hasFieldOrPropertyWithValue("useSecureCookie", false);
|
||||
assertThat(cookieSerializer).hasFieldOrPropertyWithValue("cookieMaxAge", 10);
|
||||
assertThat(cookieSerializer).hasFieldOrPropertyWithValue("sameSite", "Strict");
|
||||
assertThat(cookieSerializer).hasFieldOrPropertyWithValue("partitioned", true);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void sessionCookieSameSiteOmittedIsAppliedToAutoConfiguredCookieSerializer() {
|
||||
this.contextRunner.withUserConfiguration(SessionRepositoryConfiguration.class)
|
||||
|
||||
+4
-1
@@ -38,7 +38,8 @@ class SessionPropertiesTests {
|
||||
private final SessionProperties properties = new SessionProperties();
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
@SuppressWarnings({ "unchecked", "removal" })
|
||||
@Deprecated(since = "4.0.1", forRemoval = true)
|
||||
void determineTimeoutWithTimeoutIgnoreFallback() {
|
||||
this.properties.setTimeout(Duration.ofMinutes(1));
|
||||
Supplier<Duration> fallback = mock(Supplier.class);
|
||||
@@ -47,6 +48,8 @@ class SessionPropertiesTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("removal")
|
||||
@Deprecated(since = "4.0.1", forRemoval = true)
|
||||
void determineTimeoutWithNoTimeoutUseFallback() {
|
||||
this.properties.setTimeout(null);
|
||||
Duration fallback = Duration.ofMinutes(2);
|
||||
|
||||
Reference in New Issue
Block a user