Do not use servlet session timeout for reactive web applications

This commit fixes the auto-configuration of Spring Session to use
"server.servlet.session.timeout" as a fallback for Servlet-based web
applications only.

Closes gh-23752
This commit is contained in:
Stephane Nicoll 2020-10-26 11:58:49 +01:00
parent a669e1c568
commit e0f123e676
14 changed files with 178 additions and 48 deletions

View File

@ -24,6 +24,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
@ -52,8 +53,9 @@ class HazelcastSessionConfiguration {
@Autowired
public void customize(SessionProperties sessionProperties,
HazelcastSessionProperties hazelcastSessionProperties) {
Duration timeout = sessionProperties.getTimeout();
HazelcastSessionProperties hazelcastSessionProperties, ServerProperties serverProperties) {
Duration timeout = sessionProperties
.determineTimeout(() -> serverProperties.getServlet().getSession().getTimeout());
if (timeout != null) {
setMaxInactiveIntervalInSeconds((int) timeout.getSeconds());
}

View File

@ -24,6 +24,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
@ -60,8 +61,10 @@ class JdbcSessionConfiguration {
static class SpringBootJdbcHttpSessionConfiguration extends JdbcHttpSessionConfiguration {
@Autowired
void customize(SessionProperties sessionProperties, JdbcSessionProperties jdbcSessionProperties) {
Duration timeout = sessionProperties.getTimeout();
void customize(SessionProperties sessionProperties, JdbcSessionProperties jdbcSessionProperties,
ServerProperties serverProperties) {
Duration timeout = sessionProperties
.determineTimeout(() -> serverProperties.getServlet().getSession().getTimeout());
if (timeout != null) {
setMaxInactiveIntervalInSeconds((int) timeout.getSeconds());
}

View File

@ -22,6 +22,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
@ -48,8 +49,10 @@ class MongoSessionConfiguration {
public static class SpringBootMongoHttpSessionConfiguration extends MongoHttpSessionConfiguration {
@Autowired
public void customize(SessionProperties sessionProperties, MongoSessionProperties mongoSessionProperties) {
Duration timeout = sessionProperties.getTimeout();
public void customize(SessionProperties sessionProperties, MongoSessionProperties mongoSessionProperties,
ServerProperties serverProperties) {
Duration timeout = sessionProperties
.determineTimeout(() -> serverProperties.getServlet().getSession().getTimeout());
if (timeout != null) {
setMaxInactiveIntervalInSeconds((int) timeout.getSeconds());
}

View File

@ -22,6 +22,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
@ -68,8 +69,10 @@ class RedisSessionConfiguration {
public static class SpringBootRedisHttpSessionConfiguration extends RedisHttpSessionConfiguration {
@Autowired
public void customize(SessionProperties sessionProperties, RedisSessionProperties redisSessionProperties) {
Duration timeout = sessionProperties.getTimeout();
public void customize(SessionProperties sessionProperties, RedisSessionProperties redisSessionProperties,
ServerProperties serverProperties) {
Duration timeout = sessionProperties
.determineTimeout(() -> serverProperties.getServlet().getSession().getTimeout());
if (timeout != null) {
setMaxInactiveIntervalInSeconds((int) timeout.getSeconds());
}

View File

@ -21,14 +21,11 @@ import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Supplier;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.convert.DurationUnit;
import org.springframework.boot.web.servlet.DispatcherType;
import org.springframework.boot.web.servlet.server.Session;
import org.springframework.session.web.http.SessionRepositoryFilter;
/**
@ -55,13 +52,6 @@ public class SessionProperties {
private Servlet servlet = new Servlet();
private ServerProperties serverProperties;
@Autowired
void setServerProperties(ObjectProvider<ServerProperties> serverProperties) {
this.serverProperties = serverProperties.getIfUnique();
}
public StoreType getStoreType() {
return this.storeType;
}
@ -70,14 +60,8 @@ public class SessionProperties {
this.storeType = storeType;
}
/**
* Return the session timeout.
* @return the session timeout
* @see Session#getTimeout()
*/
public Duration getTimeout() {
return (this.timeout == null && this.serverProperties != null)
? this.serverProperties.getServlet().getSession().getTimeout() : this.timeout;
return this.timeout;
}
public void setTimeout(Duration timeout) {
@ -92,6 +76,17 @@ public class SessionProperties {
this.servlet = servlet;
}
/**
* Determine the session timeout. If no timeout is configured, the
* {@code fallbackTimeout} is used.
* @param fallbackTimeout a fallback timeout value if the timeout isn't configured
* @return the session timeout
* @since 2.4.0
*/
public Duration determineTimeout(Supplier<Duration> fallbackTimeout) {
return (this.timeout != null) ? this.timeout : fallbackTimeout.get();
}
/**
* Servlet-related properties.
*/

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@ -61,6 +61,19 @@ class ReactiveSessionAutoConfigurationMongoTests extends AbstractSessionAutoConf
.run(validateSpringSessionUsesMongo("sessions"));
}
@Test
void defaultConfigWithCustomTimeout() {
this.contextRunner.withPropertyValues("spring.session.store-type=mongodb", "spring.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
@ -77,6 +90,8 @@ class ReactiveSessionAutoConfigurationMongoTests extends AbstractSessionAutoConf
ReactiveMongoSessionRepository repository = validateSessionRepository(context,
ReactiveMongoSessionRepository.class);
assertThat(repository.getCollectionName()).isEqualTo(collectionName);
assertThat(repository).hasFieldOrPropertyWithValue("maxInactiveIntervalInSeconds",
ReactiveMongoSessionRepository.DEFAULT_INACTIVE_INTERVAL);
};
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@ -25,6 +25,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.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.data.mongo.ReactiveMongoSessionRepository;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
@ -59,6 +60,18 @@ class ReactiveSessionAutoConfigurationRedisTests extends AbstractSessionAutoConf
.run(validateSpringSessionUsesRedis("spring:session:", SaveMode.ON_SET_ATTRIBUTE));
}
@Test
void defaultConfigWithCustomTimeout() {
this.contextRunner.withPropertyValues("spring.session.store-type=redis", "spring.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
@ -74,6 +87,8 @@ class ReactiveSessionAutoConfigurationRedisTests extends AbstractSessionAutoConf
return (context) -> {
ReactiveRedisSessionRepository repository = validateSessionRepository(context,
ReactiveRedisSessionRepository.class);
assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval",
MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS);
assertThat(repository).hasFieldOrPropertyWithValue("namespace", namespace);
assertThat(repository).hasFieldOrPropertyWithValue("saveMode", saveMode);
};

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@ -21,6 +21,7 @@ import com.hazelcast.map.IMap;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
@ -63,8 +64,21 @@ class SessionAutoConfigurationHazelcastTests extends AbstractSessionAutoConfigur
.run(this::validateDefaultConfig);
}
@Test
void defaultConfigWithCustomTimeout() {
this.contextRunner.withPropertyValues("spring.session.store-type=hazelcast", "spring.session.timeout=1m")
.run((context) -> {
Hazelcast4IndexedSessionRepository repository = validateSessionRepository(context,
Hazelcast4IndexedSessionRepository.class);
assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval", 60);
});
}
private void validateDefaultConfig(AssertableWebApplicationContext context) {
validateSessionRepository(context, Hazelcast4IndexedSessionRepository.class);
Hazelcast4IndexedSessionRepository repository = validateSessionRepository(context,
Hazelcast4IndexedSessionRepository.class);
assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval",
(int) new ServerProperties().getServlet().getSession().getTimeout().getSeconds());
HazelcastInstance hazelcastInstance = context.getBean(HazelcastInstance.class);
verify(hazelcastInstance, times(1)).getMap("spring:session:sessions");
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@ -23,6 +23,7 @@ import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration;
import org.springframework.boot.autoconfigure.session.JdbcSessionConfiguration.SpringBootJdbcHttpSessionConfiguration;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.jdbc.DataSourceInitializationMode;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
@ -70,6 +71,8 @@ class SessionAutoConfigurationJdbcTests extends AbstractSessionAutoConfiguration
private void validateDefaultConfig(AssertableWebApplicationContext context) {
JdbcIndexedSessionRepository repository = validateSessionRepository(context,
JdbcIndexedSessionRepository.class);
assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval",
(int) new ServerProperties().getServlet().getSession().getTimeout().getSeconds());
assertThat(repository).hasFieldOrPropertyWithValue("tableName", "SPRING_SESSION");
assertThat(context.getBean(JdbcSessionProperties.class).getInitializeSchema())
.isEqualTo(DataSourceInitializationMode.EMBEDDED);
@ -104,6 +107,16 @@ class SessionAutoConfigurationJdbcTests extends AbstractSessionAutoConfiguration
});
}
@Test
void customTimeout() {
this.contextRunner.withPropertyValues("spring.session.store-type=jdbc", "spring.session.timeout=1m")
.run((context) -> {
JdbcIndexedSessionRepository repository = validateSessionRepository(context,
JdbcIndexedSessionRepository.class);
assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval", 60);
});
}
@Test
void customTableName() {
this.contextRunner

View File

@ -26,6 +26,7 @@ import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
import org.springframework.boot.test.context.runner.ContextConsumer;
@ -68,6 +69,12 @@ class SessionAutoConfigurationMongoTests extends AbstractSessionAutoConfiguratio
.run(validateSpringSessionUsesMongo("sessions"));
}
@Test
void defaultConfigWithCustomTimeout() {
this.contextRunner.withPropertyValues("spring.session.store-type=mongodb", "spring.session.timeout=1m")
.run(validateSpringSessionUsesMongo("sessions", Duration.ofMinutes(1)));
}
@Test
void mongoSessionStoreWithCustomizations() {
this.contextRunner
@ -76,10 +83,18 @@ class SessionAutoConfigurationMongoTests extends AbstractSessionAutoConfiguratio
}
private ContextConsumer<AssertableWebApplicationContext> validateSpringSessionUsesMongo(String collectionName) {
return validateSpringSessionUsesMongo(collectionName,
new ServerProperties().getServlet().getSession().getTimeout());
}
private ContextConsumer<AssertableWebApplicationContext> validateSpringSessionUsesMongo(String collectionName,
Duration timeout) {
return (context) -> {
MongoIndexedSessionRepository repository = validateSessionRepository(context,
MongoIndexedSessionRepository.class);
assertThat(repository).hasFieldOrPropertyWithValue("collectionName", collectionName);
assertThat(repository).hasFieldOrPropertyWithValue("maxInactiveIntervalInSeconds",
(int) timeout.getSeconds());
};
}

View File

@ -26,6 +26,7 @@ import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.autoconfigure.session.RedisSessionConfiguration.SpringBootRedisHttpSessionConfiguration;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
import org.springframework.boot.test.context.runner.ContextConsumer;
@ -83,6 +84,18 @@ class SessionAutoConfigurationRedisTests extends AbstractSessionAutoConfiguratio
SaveMode.ON_SET_ATTRIBUTE, "0 * * * * *"));
}
@Test
void defaultConfigWithCustomTimeout() {
this.contextRunner
.withPropertyValues("spring.session.store-type=redis", "spring.redis.host=" + redis.getHost(),
"spring.redis.port=" + redis.getFirstMappedPort(), "spring.session.timeout=1m")
.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class)).run((context) -> {
RedisIndexedSessionRepository repository = validateSessionRepository(context,
RedisIndexedSessionRepository.class);
assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval", 60);
});
}
@Test
void redisSessionStoreWithCustomizations() {
this.contextRunner.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class))
@ -126,6 +139,8 @@ class SessionAutoConfigurationRedisTests extends AbstractSessionAutoConfiguratio
return (context) -> {
RedisIndexedSessionRepository repository = validateSessionRepository(context,
RedisIndexedSessionRepository.class);
assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval",
(int) new ServerProperties().getServlet().getSession().getTimeout().getSeconds());
assertThat(repository.getSessionCreatedChannelPrefix()).isEqualTo(sessionCreatedChannelPrefix);
assertThat(repository).hasFieldOrPropertyWithValue("flushMode", flushMode);
SpringBootRedisHttpSessionConfiguration configuration = context

View File

@ -95,22 +95,6 @@ class SessionAutoConfigurationTests extends AbstractSessionAutoConfigurationTest
});
}
@Test
void autoConfigWhenSpringSessionTimeoutIsSetShouldUseThat() {
this.contextRunner
.withUserConfiguration(ServerPropertiesConfiguration.class, SessionRepositoryConfiguration.class)
.withPropertyValues("server.servlet.session.timeout=1", "spring.session.timeout=3")
.run((context) -> assertThat(context.getBean(SessionProperties.class).getTimeout()).hasSeconds(3));
}
@Test
void autoConfigWhenSpringSessionTimeoutIsNotSetShouldUseServerSessionTimeout() {
this.contextRunner
.withUserConfiguration(ServerPropertiesConfiguration.class, SessionRepositoryConfiguration.class)
.withPropertyValues("server.servlet.session.timeout=3")
.run((context) -> assertThat(context.getBean(SessionProperties.class).getTimeout()).hasSeconds(3));
}
@Test
void filterIsRegisteredWithAsyncErrorAndRequestDispatcherTypes() {
this.contextRunner.withUserConfiguration(SessionRepositoryConfiguration.class).run((context) -> {

View File

@ -0,0 +1,53 @@
/*
* Copyright 2012-2020 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.session;
import java.time.Duration;
import java.util.function.Supplier;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyNoInteractions;
/**
* Tests for {@link SessionProperties}.
*
* @author Stephane Nicoll
*/
class SessionPropertiesTests {
@Test
@SuppressWarnings("unchecked")
void determineTimeoutWithTimeoutIgnoreFallback() {
SessionProperties properties = new SessionProperties();
properties.setTimeout(Duration.ofMinutes(1));
Supplier<Duration> fallback = mock(Supplier.class);
assertThat(properties.determineTimeout(fallback)).isEqualTo(Duration.ofMinutes(1));
verifyNoInteractions(fallback);
}
@Test
void determineTimeoutWithNoTimeoutUseFallback() {
SessionProperties properties = new SessionProperties();
properties.setTimeout(null);
Duration fallback = Duration.ofMinutes(2);
assertThat(properties.determineTimeout(() -> fallback)).isSameAs(fallback);
}
}

View File

@ -6736,7 +6736,7 @@ For instance, it is possible to customize the name of the table for the JDBC sto
----
For setting the timeout of the session you can use the configprop:spring.session.timeout[] property.
If that property is not set, the auto-configuration falls back to the value of configprop:server.servlet.session.timeout[].
If that property is not set with a Servlet web appplication, the auto-configuration falls back to the value of configprop:server.servlet.session.timeout[].