Create spring-boot-session modules
This commit is contained in:
committed by
Phillip Webb
parent
014240d576
commit
e288c81b7b
@@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"properties": [
|
||||
{
|
||||
"name": "spring.session.redis.cleanup-cron",
|
||||
"defaultValue": "0 * * * * *"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
org.springframework.boot.session.data.redis.autoconfigure.RedisSessionAutoConfiguration
|
||||
Reference in New Issue
Block a user