Create spring-boot-session modules

This commit is contained in:
Andy Wilkinson
2025-04-03 15:59:55 +01:00
committed by Phillip Webb
parent 014240d576
commit e288c81b7b
69 changed files with 914 additions and 710 deletions

View File

@@ -0,0 +1,223 @@
/*
* Copyright 2012-2025 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.data.redis.autoconfigure;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import com.redis.testcontainers.RedisContainer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import reactor.core.publisher.Mono;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.data.redis.autoconfigure.RedisAutoConfiguration;
import org.springframework.boot.data.redis.autoconfigure.RedisReactiveAutoConfiguration;
import org.springframework.boot.session.autoconfigure.AbstractReactiveSessionAutoConfigurationTests;
import org.springframework.boot.session.autoconfigure.SessionAutoConfiguration;
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.container.TestImage;
import org.springframework.boot.webflux.autoconfigure.WebSessionIdResolverAutoConfiguration;
import org.springframework.data.redis.connection.ReactiveRedisConnection;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.http.ResponseCookie;
import org.springframework.session.MapSession;
import org.springframework.session.SaveMode;
import org.springframework.session.data.redis.ReactiveRedisIndexedSessionRepository;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
import org.springframework.session.data.redis.config.ConfigureReactiveRedisAction;
import org.springframework.session.data.redis.config.annotation.ConfigureNotifyKeyspaceEventsReactiveAction;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
/**
* Tests for the reactive support of {@link RedisSessionAutoConfiguration}.
*
* @author Stephane Nicoll
* @author Andy Wilkinson
* @author Vedran Pavic
* @author Weix Sun
*/
@Testcontainers(disabledWithoutDocker = true)
class ReactiveRedisSessionAutoConfigurationTests extends AbstractReactiveSessionAutoConfigurationTests {
@Container
public static RedisContainer redis = TestImage.container(RedisContainer.class);
@BeforeEach
void prepareRunner() {
this.contextRunner = new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(SessionAutoConfiguration.class,
WebSessionIdResolverAutoConfiguration.class, RedisSessionAutoConfiguration.class,
RedisAutoConfiguration.class, RedisReactiveAutoConfiguration.class));
}
@Test
void defaultConfig() {
this.contextRunner.run(validateSpringSessionUsesRedis("spring:session:", SaveMode.ON_SET_ATTRIBUTE));
}
@Test
void defaultConfigWithCustomTimeout() {
this.contextRunner.withPropertyValues("spring.session.timeout=1m").run((context) -> {
ReactiveRedisSessionRepository repository = validateSessionRepository(context,
ReactiveRedisSessionRepository.class);
assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval", Duration.ofMinutes(1));
});
}
@Test
void defaultConfigWithCustomWebFluxTimeout() {
this.contextRunner.withPropertyValues("server.reactive.session.timeout=1m").run((context) -> {
ReactiveRedisSessionRepository repository = validateSessionRepository(context,
ReactiveRedisSessionRepository.class);
assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval", Duration.ofMinutes(1));
});
}
@Test
void redisSessionStoreWithCustomizations() {
this.contextRunner
.withPropertyValues("spring.session.redis.namespace=foo", "spring.session.redis.save-mode=on-get-attribute")
.run(validateSpringSessionUsesRedis("foo:", SaveMode.ON_GET_ATTRIBUTE));
}
@Test
void sessionCookieConfigurationIsAppliedToAutoConfiguredWebSessionIdResolver() {
this.contextRunner.withUserConfiguration(ReactiveWebServerConfiguration.class)
.withPropertyValues("spring.data.redis.host=" + redis.getHost(),
"spring.data.redis.port=" + redis.getFirstMappedPort(),
"server.reactive.session.cookie.name:JSESSIONID",
"server.reactive.session.cookie.domain:.example.com",
"server.reactive.session.cookie.path:/example", "server.reactive.session.cookie.max-age:60",
"server.reactive.session.cookie.http-only:false", "server.reactive.session.cookie.secure:false",
"server.reactive.session.cookie.same-site:strict")
.run(assertExchangeWithSession((exchange) -> {
List<ResponseCookie> 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"));
}));
}
@Test
void indexedRedisSessionDefaultConfig() {
this.contextRunner
.withPropertyValues("spring.session.redis.repository-type=indexed",
"spring.data.redis.host=" + redis.getHost(), "spring.data.redis.port=" + redis.getFirstMappedPort())
.run(validateSpringSessionUsesIndexedRedis("spring:session:", SaveMode.ON_SET_ATTRIBUTE));
}
@Test
void indexedRedisSessionStoreWithCustomizations() {
this.contextRunner
.withPropertyValues("spring.session.redis.repository-type=indexed", "spring.session.redis.namespace=foo",
"spring.session.redis.save-mode=on-get-attribute", "spring.data.redis.host=" + redis.getHost(),
"spring.data.redis.port=" + redis.getFirstMappedPort())
.run(validateSpringSessionUsesIndexedRedis("foo:", SaveMode.ON_GET_ATTRIBUTE));
}
@Test
void indexedRedisSessionWithConfigureActionNone() {
this.contextRunner
.withPropertyValues("spring.session.redis.repository-type=indexed",
"spring.session.redis.configure-action=none", "spring.data.redis.host=" + redis.getHost(),
"spring.data.redis.port=" + redis.getFirstMappedPort())
.run(validateStrategy(ConfigureReactiveRedisAction.NO_OP.getClass()));
}
@Test
void indexedRedisSessionWithDefaultConfigureActionNone() {
this.contextRunner
.withPropertyValues("spring.session.redis.repository-type=indexed",
"spring.data.redis.host=" + redis.getHost(), "spring.data.redis.port=" + redis.getFirstMappedPort())
.run(validateStrategy(ConfigureNotifyKeyspaceEventsReactiveAction.class,
entry("notify-keyspace-events", "gxE")));
}
@Test
void indexedRedisSessionWithCustomConfigureReactiveRedisActionBean() {
this.contextRunner.withUserConfiguration(MaxEntriesReactiveRedisAction.class)
.withPropertyValues("spring.session.redis.repository-type=indexed",
"spring.data.redis.host=" + redis.getHost(), "spring.data.redis.port=" + redis.getFirstMappedPort())
.run(validateStrategy(MaxEntriesReactiveRedisAction.class, entry("set-max-intset-entries", "1024")));
}
private ContextConsumer<AssertableReactiveWebApplicationContext> validateSpringSessionUsesRedis(String namespace,
SaveMode saveMode) {
return (context) -> {
ReactiveRedisSessionRepository repository = validateSessionRepository(context,
ReactiveRedisSessionRepository.class);
assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval",
MapSession.DEFAULT_MAX_INACTIVE_INTERVAL);
assertThat(repository).hasFieldOrPropertyWithValue("namespace", namespace);
assertThat(repository).hasFieldOrPropertyWithValue("saveMode", saveMode);
};
}
private ContextConsumer<AssertableReactiveWebApplicationContext> validateSpringSessionUsesIndexedRedis(
String keyNamespace, SaveMode saveMode) {
return (context) -> {
ReactiveRedisIndexedSessionRepository repository = validateSessionRepository(context,
ReactiveRedisIndexedSessionRepository.class);
assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval",
new ServerProperties().getReactive().getSession().getTimeout());
assertThat(repository).hasFieldOrPropertyWithValue("namespace", keyNamespace);
assertThat(repository).hasFieldOrPropertyWithValue("saveMode", saveMode);
};
}
private ContextConsumer<AssertableReactiveWebApplicationContext> validateStrategy(
Class<? extends ConfigureReactiveRedisAction> expectedConfigureReactiveRedisActionType,
Map.Entry<?, ?>... expectedConfig) {
return (context) -> {
assertThat(context).hasSingleBean(ConfigureReactiveRedisAction.class);
assertThat(context).hasSingleBean(RedisConnectionFactory.class);
assertThat(context.getBean(ConfigureReactiveRedisAction.class))
.isInstanceOf(expectedConfigureReactiveRedisActionType);
ReactiveRedisConnection connection = context.getBean(ReactiveRedisConnectionFactory.class)
.getReactiveConnection();
if (expectedConfig.length > 0) {
assertThat(connection.serverCommands().getConfig("*").block(Duration.ofSeconds(30)))
.contains(expectedConfig);
}
};
}
static class MaxEntriesReactiveRedisAction implements ConfigureReactiveRedisAction {
@Override
public Mono<Void> configure(ReactiveRedisConnection connection) {
return Mono.when(connection.serverCommands().setConfig("set-max-intset-entries", "1024"));
}
}
}

View File

@@ -0,0 +1,252 @@
/*
* Copyright 2012-2025 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.data.redis.autoconfigure;
import java.time.Duration;
import java.util.Map;
import com.redis.testcontainers.RedisContainer;
import org.junit.jupiter.api.BeforeEach;
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.web.ServerProperties;
import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException;
import org.springframework.boot.data.redis.autoconfigure.RedisAutoConfiguration;
import org.springframework.boot.session.autoconfigure.AbstractSessionAutoConfigurationTests;
import org.springframework.boot.session.autoconfigure.SessionAutoConfiguration;
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
import org.springframework.boot.test.context.runner.ContextConsumer;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.testsupport.container.TestImage;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.session.FlushMode;
import org.springframework.session.SaveMode;
import org.springframework.session.config.SessionRepositoryCustomizer;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.RedisSessionRepository;
import org.springframework.session.data.redis.config.ConfigureNotifyKeyspaceEventsAction;
import org.springframework.session.data.redis.config.ConfigureRedisAction;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
/**
* Tests for {@link RedisSessionAutoConfiguration}.
*
* @author Stephane Nicoll
* @author Vedran Pavic
*/
@Testcontainers(disabledWithoutDocker = true)
class RedisSessionAutoConfigurationTests extends AbstractSessionAutoConfigurationTests {
@Container
public static RedisContainer redis = TestImage.container(RedisContainer.class);
@BeforeEach
void prepareContextRunner() {
this.contextRunner = new WebApplicationContextRunner().withConfiguration(AutoConfigurations
.of(SessionAutoConfiguration.class, RedisSessionAutoConfiguration.class, RedisAutoConfiguration.class));
}
@Test
void defaultConfig() {
this.contextRunner
.withPropertyValues("spring.data.redis.host=" + redis.getHost(),
"spring.data.redis.port=" + redis.getFirstMappedPort())
.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class))
.run(validateSpringSessionUsesDefaultRedis("spring:session:", FlushMode.ON_SAVE,
SaveMode.ON_SET_ATTRIBUTE));
}
@Test
void invalidConfigurationPropertyValueWhenDefaultConfigIsUsedWithCustomCronCleanup() {
this.contextRunner.withPropertyValues("spring.data.redis.host=" + redis.getHost(),
"spring.data.redis.port=" + redis.getFirstMappedPort(), "spring.session.redis.cleanup-cron=0 0 * * * *")
.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class))
.run((context) -> {
assertThat(context).hasFailed();
assertThat(context.getStartupFailure())
.hasRootCauseExactlyInstanceOf(InvalidConfigurationPropertyValueException.class);
});
}
@Test
void defaultConfigWithCustomTimeout() {
this.contextRunner
.withPropertyValues("spring.data.redis.host=" + redis.getHost(),
"spring.data.redis.port=" + redis.getFirstMappedPort(), "spring.session.timeout=1m")
.run((context) -> {
RedisSessionRepository repository = validateSessionRepository(context, RedisSessionRepository.class);
assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval", Duration.ofMinutes(1));
});
}
@Test
void defaultRedisSessionStoreWithCustomizations() {
this.contextRunner
.withPropertyValues("spring.session.redis.namespace=foo", "spring.session.redis.flush-mode=immediate",
"spring.session.redis.save-mode=on-get-attribute", "spring.data.redis.host=" + redis.getHost(),
"spring.data.redis.port=" + redis.getFirstMappedPort())
.run(validateSpringSessionUsesDefaultRedis("foo:", FlushMode.IMMEDIATE, SaveMode.ON_GET_ATTRIBUTE));
}
@Test
void indexedRedisSessionDefaultConfig() {
this.contextRunner
.withPropertyValues("spring.session.redis.repository-type=indexed",
"spring.data.redis.host=" + redis.getHost(), "spring.data.redis.port=" + redis.getFirstMappedPort())
.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class))
.run(validateSpringSessionUsesIndexedRedis("spring:session:", FlushMode.ON_SAVE, SaveMode.ON_SET_ATTRIBUTE,
"0 * * * * *"));
}
@Test
void indexedRedisSessionStoreWithCustomizations() {
this.contextRunner
.withPropertyValues("spring.session.redis.repository-type=indexed", "spring.session.redis.namespace=foo",
"spring.session.redis.flush-mode=immediate", "spring.session.redis.save-mode=on-get-attribute",
"spring.session.redis.cleanup-cron=0 0 12 * * *", "spring.data.redis.host=" + redis.getHost(),
"spring.data.redis.port=" + redis.getFirstMappedPort())
.run(validateSpringSessionUsesIndexedRedis("foo:", FlushMode.IMMEDIATE, SaveMode.ON_GET_ATTRIBUTE,
"0 0 12 * * *"));
}
@Test
void indexedRedisSessionWithConfigureActionNone() {
this.contextRunner
.withPropertyValues("spring.session.redis.repository-type=indexed",
"spring.session.redis.configure-action=none", "spring.data.redis.host=" + redis.getHost(),
"spring.data.redis.port=" + redis.getFirstMappedPort())
.run(validateStrategy(ConfigureRedisAction.NO_OP.getClass()));
}
@Test
void indexedRedisSessionWithDefaultConfigureActionNone() {
this.contextRunner
.withPropertyValues("spring.session.redis.repository-type=indexed",
"spring.data.redis.host=" + redis.getHost(), "spring.data.redis.port=" + redis.getFirstMappedPort())
.run(validateStrategy(ConfigureNotifyKeyspaceEventsAction.class, entry("notify-keyspace-events", "gxE")));
}
@Test
void indexedRedisSessionWithCustomConfigureRedisActionBean() {
this.contextRunner.withUserConfiguration(MaxEntriesRedisAction.class)
.withPropertyValues("spring.session.redis.repository-type=indexed",
"spring.data.redis.host=" + redis.getHost(), "spring.data.redis.port=" + redis.getFirstMappedPort())
.run(validateStrategy(MaxEntriesRedisAction.class, entry("set-max-intset-entries", "1024")));
}
@Test
void whenTheUserDefinesTheirOwnSessionRepositoryCustomizerThenDefaultConfigurationIsOverwritten() {
this.contextRunner.withUserConfiguration(CustomizerConfiguration.class)
.withPropertyValues("spring.session.redis.flush-mode=immediate",
"spring.data.redis.host=" + redis.getHost(), "spring.data.redis.port=" + redis.getFirstMappedPort())
.run((context) -> {
RedisSessionRepository repository = validateSessionRepository(context, RedisSessionRepository.class);
assertThat(repository).hasFieldOrPropertyWithValue("flushMode", FlushMode.ON_SAVE);
});
}
@Test
void whenIndexedAndTheUserDefinesTheirOwnSessionRepositoryCustomizerThenDefaultConfigurationIsOverwritten() {
this.contextRunner.withUserConfiguration(IndexedCustomizerConfiguration.class)
.withPropertyValues("spring.session.redis.repository-type=indexed",
"spring.session.redis.flush-mode=immediate", "spring.data.redis.host=" + redis.getHost(),
"spring.data.redis.port=" + redis.getFirstMappedPort())
.run((context) -> {
RedisIndexedSessionRepository repository = validateSessionRepository(context,
RedisIndexedSessionRepository.class);
assertThat(repository).hasFieldOrPropertyWithValue("flushMode", FlushMode.ON_SAVE);
});
}
private ContextConsumer<AssertableWebApplicationContext> validateSpringSessionUsesDefaultRedis(String keyNamespace,
FlushMode flushMode, SaveMode saveMode) {
return (context) -> {
RedisSessionRepository repository = validateSessionRepository(context, RedisSessionRepository.class);
assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval",
new ServerProperties().getServlet().getSession().getTimeout());
assertThat(repository).hasFieldOrPropertyWithValue("keyNamespace", keyNamespace);
assertThat(repository).hasFieldOrPropertyWithValue("flushMode", flushMode);
assertThat(repository).hasFieldOrPropertyWithValue("saveMode", saveMode);
};
}
private ContextConsumer<AssertableWebApplicationContext> validateSpringSessionUsesIndexedRedis(String keyNamespace,
FlushMode flushMode, SaveMode saveMode, String cleanupCron) {
return (context) -> {
RedisIndexedSessionRepository repository = validateSessionRepository(context,
RedisIndexedSessionRepository.class);
assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval",
new ServerProperties().getServlet().getSession().getTimeout());
assertThat(repository).hasFieldOrPropertyWithValue("namespace", keyNamespace);
assertThat(repository).hasFieldOrPropertyWithValue("flushMode", flushMode);
assertThat(repository).hasFieldOrPropertyWithValue("saveMode", saveMode);
assertThat(repository).hasFieldOrPropertyWithValue("cleanupCron", cleanupCron);
};
}
private ContextConsumer<AssertableWebApplicationContext> validateStrategy(
Class<? extends ConfigureRedisAction> expectedConfigureRedisActionType, Map.Entry<?, ?>... expectedConfig) {
return (context) -> {
assertThat(context).hasSingleBean(ConfigureRedisAction.class);
assertThat(context).hasSingleBean(RedisConnectionFactory.class);
assertThat(context.getBean(ConfigureRedisAction.class)).isInstanceOf(expectedConfigureRedisActionType);
RedisConnection connection = context.getBean(RedisConnectionFactory.class).getConnection();
if (expectedConfig.length > 0) {
assertThat(connection.serverCommands().getConfig("*")).contains(expectedConfig);
}
};
}
static class MaxEntriesRedisAction implements ConfigureRedisAction {
@Override
public void configure(RedisConnection connection) {
connection.serverCommands().setConfig("set-max-intset-entries", "1024");
}
}
@Configuration(proxyBeanMethods = false)
static class CustomizerConfiguration {
@Bean
SessionRepositoryCustomizer<RedisSessionRepository> sessionRepositoryCustomizer() {
return (repository) -> repository.setFlushMode(FlushMode.ON_SAVE);
}
}
@Configuration(proxyBeanMethods = false)
static class IndexedCustomizerConfiguration {
@Bean
SessionRepositoryCustomizer<RedisIndexedSessionRepository> sessionRepositoryCustomizer() {
return (repository) -> repository.setFlushMode(FlushMode.ON_SAVE);
}
}
}

View File

@@ -0,0 +1,211 @@
/*
* Copyright 2012-2025 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.data.redis.autoconfigure;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
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.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException;
import org.springframework.boot.data.redis.autoconfigure.RedisAutoConfiguration;
import org.springframework.boot.data.redis.autoconfigure.RedisReactiveAutoConfiguration;
import org.springframework.boot.session.autoconfigure.SessionAutoConfiguration;
import org.springframework.boot.session.autoconfigure.SessionProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.SessionRepository;
import org.springframework.session.config.ReactiveSessionRepositoryCustomizer;
import org.springframework.session.config.SessionRepositoryCustomizer;
import org.springframework.session.data.redis.ReactiveRedisIndexedSessionRepository;
import org.springframework.session.data.redis.ReactiveRedisSessionRepository;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.data.redis.RedisSessionRepository;
import org.springframework.session.data.redis.config.ConfigureNotifyKeyspaceEventsAction;
import org.springframework.session.data.redis.config.ConfigureReactiveRedisAction;
import org.springframework.session.data.redis.config.ConfigureRedisAction;
import org.springframework.session.data.redis.config.annotation.ConfigureNotifyKeyspaceEventsReactiveAction;
import org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration;
import org.springframework.session.data.redis.config.annotation.web.http.RedisIndexedHttpSessionConfiguration;
import org.springframework.session.data.redis.config.annotation.web.server.RedisIndexedWebSessionConfiguration;
import org.springframework.session.data.redis.config.annotation.web.server.RedisWebSessionConfiguration;
/**
* {@link EnableAutoConfiguration Auto-configuration} for Spring Session Data Redis.
*
* @author Andy Wilkinson
* @author Tommy Ludwig
* @author Eddú Meléndez
* @author Stephane Nicoll
* @author Vedran Pavic
* @since 4.0.0
*/
@AutoConfiguration(before = SessionAutoConfiguration.class,
beforeName = { "org.springframework.boot.webflux.autoconfigure.HttpHandlerAutoConfiguration",
"org.springframework.boot.webflux.autoconfigure.WebFluxAutoConfiguration" },
after = { RedisAutoConfiguration.class, RedisReactiveAutoConfiguration.class },
afterName = "org.springframework.boot.webflux.autoconfigure.WebSessionIdResolverAutoConfiguration")
@EnableConfigurationProperties({ RedisSessionProperties.class, ServerProperties.class, SessionProperties.class })
public class RedisSessionAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ RedisTemplate.class, RedisIndexedSessionRepository.class })
@ConditionalOnMissingBean(SessionRepository.class)
@ConditionalOnBean(RedisConnectionFactory.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
static class ServletRedisSessionConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.session.redis.repository-type", havingValue = "default",
matchIfMissing = true)
@Import(RedisHttpSessionConfiguration.class)
static class DefaultRedisSessionConfiguration {
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
SessionRepositoryCustomizer<RedisSessionRepository> springBootSessionRepositoryCustomizer(
SessionProperties sessionProperties, RedisSessionProperties redisSessionProperties,
ServerProperties serverProperties) {
String cleanupCron = redisSessionProperties.getCleanupCron();
if (cleanupCron != null) {
throw new InvalidConfigurationPropertyValueException("spring.session.redis.cleanup-cron",
cleanupCron, "Cron-based cleanup is only supported when "
+ "spring.session.redis.repository-type is set to indexed.");
}
return (sessionRepository) -> {
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(sessionProperties
.determineTimeout(() -> serverProperties.getServlet().getSession().getTimeout()))
.to(sessionRepository::setDefaultMaxInactiveInterval);
map.from(redisSessionProperties::getNamespace).to(sessionRepository::setRedisKeyNamespace);
map.from(redisSessionProperties::getFlushMode).to(sessionRepository::setFlushMode);
map.from(redisSessionProperties::getSaveMode).to(sessionRepository::setSaveMode);
};
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.session.redis.repository-type", havingValue = "indexed")
@Import(RedisIndexedHttpSessionConfiguration.class)
static class IndexedRedisSessionConfiguration {
@Bean
@ConditionalOnMissingBean
ConfigureRedisAction configureRedisAction(RedisSessionProperties redisSessionProperties) {
return switch (redisSessionProperties.getConfigureAction()) {
case NOTIFY_KEYSPACE_EVENTS -> new ConfigureNotifyKeyspaceEventsAction();
case NONE -> ConfigureRedisAction.NO_OP;
};
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
SessionRepositoryCustomizer<RedisIndexedSessionRepository> springBootSessionRepositoryCustomizer(
SessionProperties sessionProperties, RedisSessionProperties redisSessionProperties,
ServerProperties serverProperties) {
return (sessionRepository) -> {
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(sessionProperties
.determineTimeout(() -> serverProperties.getServlet().getSession().getTimeout()))
.to(sessionRepository::setDefaultMaxInactiveInterval);
map.from(redisSessionProperties::getNamespace).to(sessionRepository::setRedisKeyNamespace);
map.from(redisSessionProperties::getFlushMode).to(sessionRepository::setFlushMode);
map.from(redisSessionProperties::getSaveMode).to(sessionRepository::setSaveMode);
map.from(redisSessionProperties::getCleanupCron).to(sessionRepository::setCleanupCron);
};
}
}
}
@ConditionalOnWebApplication(type = Type.REACTIVE)
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ ReactiveRedisConnectionFactory.class, ReactiveRedisSessionRepository.class })
@ConditionalOnMissingBean(ReactiveSessionRepository.class)
@ConditionalOnBean(ReactiveRedisConnectionFactory.class)
class ReactiveRedisSessionConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.session.redis.repository-type", havingValue = "default",
matchIfMissing = true)
@Import(RedisWebSessionConfiguration.class)
static class DefaultRedisSessionConfiguration {
@Bean
ReactiveSessionRepositoryCustomizer<ReactiveRedisSessionRepository> springBootSessionRepositoryCustomizer(
SessionProperties sessionProperties, RedisSessionProperties redisSessionProperties,
ServerProperties serverProperties) {
return (sessionRepository) -> {
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(sessionProperties
.determineTimeout(() -> serverProperties.getReactive().getSession().getTimeout()))
.to(sessionRepository::setDefaultMaxInactiveInterval);
map.from(redisSessionProperties::getNamespace).to(sessionRepository::setRedisKeyNamespace);
map.from(redisSessionProperties::getSaveMode).to(sessionRepository::setSaveMode);
};
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.session.redis.repository-type", havingValue = "indexed")
@Import(RedisIndexedWebSessionConfiguration.class)
static class IndexedRedisSessionConfiguration {
@Bean
@ConditionalOnMissingBean
ConfigureReactiveRedisAction configureReactiveRedisAction(RedisSessionProperties redisSessionProperties) {
return switch (redisSessionProperties.getConfigureAction()) {
case NOTIFY_KEYSPACE_EVENTS -> new ConfigureNotifyKeyspaceEventsReactiveAction();
case NONE -> ConfigureReactiveRedisAction.NO_OP;
};
}
@Bean
ReactiveSessionRepositoryCustomizer<ReactiveRedisIndexedSessionRepository> springBootSessionRepositoryCustomizer(
SessionProperties sessionProperties, RedisSessionProperties redisSessionProperties,
ServerProperties serverProperties) {
return (sessionRepository) -> {
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(sessionProperties
.determineTimeout(() -> serverProperties.getReactive().getSession().getTimeout()))
.to(sessionRepository::setDefaultMaxInactiveInterval);
map.from(redisSessionProperties::getNamespace).to(sessionRepository::setRedisKeyNamespace);
map.from(redisSessionProperties::getSaveMode).to(sessionRepository::setSaveMode);
};
}
}
}
}

View File

@@ -0,0 +1,151 @@
/*
* Copyright 2012-2025 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.data.redis.autoconfigure;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.session.FlushMode;
import org.springframework.session.SaveMode;
/**
* Configuration properties for Redis backed Spring Session.
*
* @author Vedran Pavic
* @since 4.0.0
*/
@ConfigurationProperties("spring.session.redis")
public class RedisSessionProperties {
/**
* Namespace for keys used to store sessions.
*/
private String namespace = "spring:session";
/**
* Sessions flush mode. Determines when session changes are written to the session
* store. Not supported with a reactive session repository.
*/
private FlushMode flushMode = FlushMode.ON_SAVE;
/**
* Sessions save mode. Determines how session changes are tracked and saved to the
* session store.
*/
private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
/**
* The configure action to apply when no user-defined ConfigureRedisAction or
* ConfigureReactiveRedisAction bean is present.
*/
private ConfigureAction configureAction = ConfigureAction.NOTIFY_KEYSPACE_EVENTS;
/**
* Cron expression for expired session cleanup job. Only supported when
* repository-type is set to indexed. Not supported with a reactive session
* repository.
*/
private String cleanupCron;
/**
* Type of Redis session repository to configure.
*/
private RepositoryType repositoryType = RepositoryType.DEFAULT;
public String getNamespace() {
return this.namespace;
}
public void setNamespace(String namespace) {
this.namespace = namespace;
}
public FlushMode getFlushMode() {
return this.flushMode;
}
public void setFlushMode(FlushMode flushMode) {
this.flushMode = flushMode;
}
public SaveMode getSaveMode() {
return this.saveMode;
}
public void setSaveMode(SaveMode saveMode) {
this.saveMode = saveMode;
}
public String getCleanupCron() {
return this.cleanupCron;
}
public void setCleanupCron(String cleanupCron) {
this.cleanupCron = cleanupCron;
}
public ConfigureAction getConfigureAction() {
return this.configureAction;
}
public void setConfigureAction(ConfigureAction configureAction) {
this.configureAction = configureAction;
}
public RepositoryType getRepositoryType() {
return this.repositoryType;
}
public void setRepositoryType(RepositoryType repositoryType) {
this.repositoryType = repositoryType;
}
/**
* Strategies for configuring and validating Redis.
*/
public enum ConfigureAction {
/**
* Ensure that Redis Keyspace events for Generic commands and Expired events are
* enabled.
*/
NOTIFY_KEYSPACE_EVENTS,
/**
* No not attempt to apply any custom Redis configuration.
*/
NONE
}
/**
* Type of Redis session repository to auto-configure.
*/
public enum RepositoryType {
/**
* Auto-configure a RedisSessionRepository or ReactiveRedisSessionRepository.
*/
DEFAULT,
/**
* Auto-configure a RedisIndexedSessionRepository or
* ReactiveRedisIndexedSessionRepository.
*/
INDEXED
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 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.
*/
/**
* Auto-configuration for Spring Session Data Redis.
*/
package org.springframework.boot.session.data.redis.autoconfigure;

View File

@@ -0,0 +1,8 @@
{
"properties": [
{
"name": "spring.session.redis.cleanup-cron",
"defaultValue": "0 * * * * *"
}
]
}

View File

@@ -0,0 +1 @@
org.springframework.boot.session.data.redis.autoconfigure.RedisSessionAutoConfiguration