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,133 @@
/*
* 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.mongodb.autoconfigure;
import java.time.Duration;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.MongoDBContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.data.mongodb.autoconfigure.MongoReactiveDataAutoConfiguration;
import org.springframework.boot.mongodb.autoconfigure.MongoReactiveAutoConfiguration;
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.http.ResponseCookie;
import org.springframework.session.MapSession;
import org.springframework.session.data.mongo.ReactiveMongoSessionRepository;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for the reactive support of {@link MongoSessionAutoConfiguration}.
*
* @author Andy Wilkinson
* @author Weix Sun
*/
@Testcontainers(disabledWithoutDocker = true)
class MongoReactiveSessionAutoConfigurationTests extends AbstractReactiveSessionAutoConfigurationTests {
@Container
static final MongoDBContainer mongoDb = TestImage.container(MongoDBContainer.class);
@BeforeEach
void prepareContextRunner() {
this.contextRunner = new ReactiveWebApplicationContextRunner().withConfiguration(
AutoConfigurations.of(SessionAutoConfiguration.class, MongoSessionAutoConfiguration.class,
MongoReactiveAutoConfiguration.class, MongoReactiveDataAutoConfiguration.class));
}
@Test
void defaultConfig() {
this.contextRunner.withPropertyValues("spring.data.mongodb.uri=" + mongoDb.getReplicaSetUrl())
.run(validateSpringSessionUsesMongo("sessions"));
}
@Test
void defaultConfigWithCustomTimeout() {
this.contextRunner
.withPropertyValues("spring.session.timeout=1m", "spring.data.mongodb.uri=" + mongoDb.getReplicaSetUrl())
.run((context) -> {
ReactiveMongoSessionRepository repository = validateSessionRepository(context,
ReactiveMongoSessionRepository.class);
assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval", Duration.ofMinutes(1));
});
}
@Test
void defaultConfigWithCustomSessionTimeout() {
this.contextRunner
.withPropertyValues("server.reactive.session.timeout=1m",
"spring.data.mongodb.uri=" + mongoDb.getReplicaSetUrl())
.run((context) -> {
ReactiveMongoSessionRepository repository = validateSessionRepository(context,
ReactiveMongoSessionRepository.class);
assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval", Duration.ofMinutes(1));
});
}
@Test
void mongoSessionStoreWithCustomizations() {
this.contextRunner
.withPropertyValues("spring.session.mongodb.collection-name=foo",
"spring.data.mongodb.uri=" + mongoDb.getReplicaSetUrl())
.run(validateSpringSessionUsesMongo("foo"));
}
@Test
void sessionCookieConfigurationIsAppliedToAutoConfiguredWebSessionIdResolver() {
this.contextRunner.withConfiguration(AutoConfigurations.of(WebSessionIdResolverAutoConfiguration.class))
.withUserConfiguration(ReactiveWebServerConfiguration.class)
.withPropertyValues("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",
"spring.data.mongodb.uri=" + mongoDb.getReplicaSetUrl())
.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"));
}));
}
private ContextConsumer<AssertableReactiveWebApplicationContext> validateSpringSessionUsesMongo(
String collectionName) {
return (context) -> {
ReactiveMongoSessionRepository repository = validateSessionRepository(context,
ReactiveMongoSessionRepository.class);
assertThat(repository.getCollectionName()).isEqualTo(collectionName);
assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval",
MapSession.DEFAULT_MAX_INACTIVE_INTERVAL);
};
}
}

View File

@@ -0,0 +1,112 @@
/*
* 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.mongodb.autoconfigure;
import java.time.Duration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.MongoDBContainer;
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.data.mongodb.autoconfigure.MongoDataAutoConfiguration;
import org.springframework.boot.mongodb.autoconfigure.MongoAutoConfiguration;
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.session.config.SessionRepositoryCustomizer;
import org.springframework.session.data.mongo.MongoIndexedSessionRepository;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for the servlet support of {@link MongoSessionAutoConfiguration}.
*
* @author Andy Wilkinson
*/
@Testcontainers(disabledWithoutDocker = true)
class MongoSessionAutoConfigurationTests extends AbstractSessionAutoConfigurationTests {
@Container
static final MongoDBContainer mongoDb = TestImage.container(MongoDBContainer.class);
@BeforeEach
void prepareContextRunner() {
this.contextRunner = new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class, MongoDataAutoConfiguration.class,
SessionAutoConfiguration.class, MongoSessionAutoConfiguration.class))
.withPropertyValues("spring.data.mongodb.uri=" + mongoDb.getReplicaSetUrl());
}
@Test
void defaultConfig() {
this.contextRunner.run(validateSpringSessionUsesMongo("sessions"));
}
@Test
void defaultConfigWithCustomTimeout() {
this.contextRunner.withPropertyValues("spring.session.timeout=1m")
.run(validateSpringSessionUsesMongo("sessions", Duration.ofMinutes(1)));
}
@Test
void mongoSessionStoreWithCustomizations() {
this.contextRunner.withPropertyValues("spring.session.mongodb.collection-name=foo")
.run(validateSpringSessionUsesMongo("foo"));
}
@Test
void whenTheUserDefinesTheirOwnSessionRepositoryCustomizerThenDefaultConfigurationIsOverwritten() {
this.contextRunner.withUserConfiguration(CustomizerConfiguration.class)
.withPropertyValues("spring.session.mongodb.collection-name=foo")
.run(validateSpringSessionUsesMongo("customized"));
}
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("defaultMaxInactiveInterval", timeout);
};
}
@Configuration(proxyBeanMethods = false)
static class CustomizerConfiguration {
@Bean
SessionRepositoryCustomizer<MongoIndexedSessionRepository> sessionRepositoryCustomizer() {
return (repository) -> repository.setCollectionName("customized");
}
}
}

View File

@@ -0,0 +1,111 @@
/*
* 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.mongodb.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.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.data.mongodb.autoconfigure.MongoDataAutoConfiguration;
import org.springframework.boot.data.mongodb.autoconfigure.MongoReactiveDataAutoConfiguration;
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.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
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.mongo.MongoIndexedSessionRepository;
import org.springframework.session.data.mongo.ReactiveMongoSessionRepository;
import org.springframework.session.data.mongo.config.annotation.web.http.MongoHttpSessionConfiguration;
import org.springframework.session.data.mongo.config.annotation.web.reactive.ReactiveMongoWebSessionConfiguration;
/**
* {@link EnableAutoConfiguration Auto-configuration} for Spring Session Data MongoDB.
*
* @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 = { MongoDataAutoConfiguration.class, MongoReactiveDataAutoConfiguration.class },
afterName = "org.springframework.boot.webflux.autoconfigure.WebSessionIdResolverAutoConfiguration")
@EnableConfigurationProperties(MongoSessionProperties.class)
public class MongoSessionAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ MongoOperations.class, MongoIndexedSessionRepository.class })
@ConditionalOnBean(MongoOperations.class)
@ConditionalOnMissingBean(SessionRepository.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@Import(MongoHttpSessionConfiguration.class)
class ServletMongoSessionConfiguration {
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
SessionRepositoryCustomizer<MongoIndexedSessionRepository> springBootSessionRepositoryCustomizer(
SessionProperties sessionProperties, MongoSessionProperties mongoSessionProperties,
ServerProperties serverProperties) {
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
return (sessionRepository) -> {
map.from(sessionProperties
.determineTimeout(() -> serverProperties.getServlet().getSession().getTimeout()))
.to(sessionRepository::setDefaultMaxInactiveInterval);
map.from(mongoSessionProperties::getCollectionName).to(sessionRepository::setCollectionName);
};
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ ReactiveMongoOperations.class, ReactiveMongoSessionRepository.class })
@ConditionalOnMissingBean(ReactiveSessionRepository.class)
@ConditionalOnBean(ReactiveMongoOperations.class)
@Import(ReactiveMongoWebSessionConfiguration.class)
class ReactiveMongoSessionConfiguration {
@Bean
ReactiveSessionRepositoryCustomizer<ReactiveMongoSessionRepository> springBootSessionRepositoryCustomizer(
SessionProperties sessionProperties, MongoSessionProperties mongoSessionProperties,
ServerProperties serverProperties) {
return (sessionRepository) -> {
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(sessionProperties
.determineTimeout(() -> serverProperties.getReactive().getSession().getTimeout()))
.to(sessionRepository::setDefaultMaxInactiveInterval);
map.from(mongoSessionProperties::getCollectionName).to(sessionRepository::setCollectionName);
};
}
}
}

View File

@@ -0,0 +1,43 @@
/*
* 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.mongodb.autoconfigure;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Configuration properties for Mongo-backed Spring Session.
*
* @author Andy Wilkinson
* @since 4.0.0
*/
@ConfigurationProperties("spring.session.mongodb")
public class MongoSessionProperties {
/**
* Collection name used to store sessions.
*/
private String collectionName = "sessions";
public String getCollectionName() {
return this.collectionName;
}
public void setCollectionName(String collectionName) {
this.collectionName = collectionName;
}
}

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 MongoDB.
*/
package org.springframework.boot.session.data.mongodb.autoconfigure;

View File

@@ -0,0 +1 @@
org.springframework.boot.session.data.mongodb.autoconfigure.MongoSessionAutoConfiguration