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,37 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.session.autoconfigure;
import org.springframework.session.web.http.DefaultCookieSerializer;
/**
* Callback interface that can be implemented by beans wishing to customize the
* {@link DefaultCookieSerializer} configuration.
*
* @author Vedran Pavic
* @since 4.0.0
*/
@FunctionalInterface
public interface DefaultCookieSerializerCustomizer {
/**
* Customize the cookie serializer.
* @param cookieSerializer the {@code DefaultCookieSerializer} to customize
*/
void customize(DefaultCookieSerializer cookieSerializer);
}

View File

@@ -0,0 +1,127 @@
/*
* 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.autoconfigure;
import java.time.Duration;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.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.web.server.Cookie;
import org.springframework.boot.web.server.Cookie.SameSite;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.session.Session;
import org.springframework.session.security.web.authentication.SpringSessionRememberMeServices;
import org.springframework.session.web.http.CookieHttpSessionIdResolver;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
import org.springframework.session.web.http.HttpSessionIdResolver;
/**
* {@link EnableAutoConfiguration Auto-configuration} for Spring Session.
*
* @author Andy Wilkinson
* @author Tommy Ludwig
* @author Eddú Meléndez
* @author Stephane Nicoll
* @author Vedran Pavic
* @author Weix Sun
* @since 4.0.0
*/
@AutoConfiguration
@ConditionalOnClass(Session.class)
@ConditionalOnWebApplication
@EnableConfigurationProperties({ ServerProperties.class, SessionProperties.class })
public class SessionAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@Import(SessionRepositoryFilterConfiguration.class)
static class ServletSessionConfiguration {
@Bean
@Conditional(DefaultCookieSerializerCondition.class)
DefaultCookieSerializer cookieSerializer(ServerProperties serverProperties,
ObjectProvider<DefaultCookieSerializerCustomizer> cookieSerializerCustomizers) {
Cookie cookie = serverProperties.getServlet().getSession().getCookie();
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(cookie::getName).to(cookieSerializer::setCookieName);
map.from(cookie::getDomain).to(cookieSerializer::setDomainName);
map.from(cookie::getPath).to(cookieSerializer::setCookiePath);
map.from(cookie::getHttpOnly).to(cookieSerializer::setUseHttpOnlyCookie);
map.from(cookie::getSecure).to(cookieSerializer::setUseSecureCookie);
map.from(cookie::getMaxAge).asInt(Duration::getSeconds).to(cookieSerializer::setCookieMaxAge);
map.from(cookie::getSameSite).as(SameSite::attributeValue).to(cookieSerializer::setSameSite);
map.from(cookie::getPartitioned).to(cookieSerializer::setPartitioned);
cookieSerializerCustomizers.orderedStream().forEach((customizer) -> customizer.customize(cookieSerializer));
return cookieSerializer;
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RememberMeServices.class)
static class RememberMeServicesConfiguration {
@Bean
DefaultCookieSerializerCustomizer rememberMeServicesCookieSerializerCustomizer() {
return (cookieSerializer) -> cookieSerializer
.setRememberMeRequestAttribute(SpringSessionRememberMeServices.REMEMBER_ME_LOGIN_ATTR);
}
}
}
/**
* Condition to trigger the creation of a {@link DefaultCookieSerializer}. This kicks
* in if either no {@link HttpSessionIdResolver} and {@link CookieSerializer} beans
* are registered, or if {@link CookieHttpSessionIdResolver} is registered but
* {@link CookieSerializer} is not.
*/
static class DefaultCookieSerializerCondition extends AnyNestedCondition {
DefaultCookieSerializerCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnMissingBean({ HttpSessionIdResolver.class, CookieSerializer.class })
static class NoComponentsAvailable {
}
@ConditionalOnBean(CookieHttpSessionIdResolver.class)
@ConditionalOnMissingBean(CookieSerializer.class)
static class CookieHttpSessionIdResolverAvailable {
}
}
}

View File

@@ -0,0 +1,110 @@
/*
* 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.autoconfigure;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Supplier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.convert.DurationUnit;
import org.springframework.boot.web.servlet.DispatcherType;
import org.springframework.session.web.http.SessionRepositoryFilter;
/**
* Configuration properties for Spring Session.
*
* @author Tommy Ludwig
* @author Stephane Nicoll
* @author Vedran Pavic
* @since 4.0.0
*/
@ConfigurationProperties("spring.session")
public class SessionProperties {
/**
* Session timeout. If a duration suffix is not specified, seconds will be used.
*/
@DurationUnit(ChronoUnit.SECONDS)
private Duration timeout;
private Servlet servlet = new Servlet();
public Duration getTimeout() {
return this.timeout;
}
public void setTimeout(Duration timeout) {
this.timeout = timeout;
}
public Servlet getServlet() {
return this.servlet;
}
public void setServlet(Servlet servlet) {
this.servlet = servlet;
}
/**
* Determine the session timeout. If no timeout is configured, the
* {@code fallbackTimeout} is used.
* @param fallbackTimeout a fallback timeout value if the timeout isn't configured
* @return the session timeout
*/
public Duration determineTimeout(Supplier<Duration> fallbackTimeout) {
return (this.timeout != null) ? this.timeout : fallbackTimeout.get();
}
/**
* Servlet-related properties.
*/
public static class Servlet {
/**
* Session repository filter order.
*/
private int filterOrder = SessionRepositoryFilter.DEFAULT_ORDER;
/**
* Session repository filter dispatcher types.
*/
private Set<DispatcherType> filterDispatcherTypes = new HashSet<>(
Arrays.asList(DispatcherType.ASYNC, DispatcherType.ERROR, DispatcherType.REQUEST));
public int getFilterOrder() {
return this.filterOrder;
}
public void setFilterOrder(int filterOrder) {
this.filterOrder = filterOrder;
}
public Set<DispatcherType> getFilterDispatcherTypes() {
return this.filterDispatcherTypes;
}
public void setFilterDispatcherTypes(Set<DispatcherType> filterDispatcherTypes) {
this.filterDispatcherTypes = filterDispatcherTypes;
}
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.session.autoconfigure;
import java.util.EnumSet;
import java.util.stream.Collectors;
import jakarta.servlet.DispatcherType;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.util.Assert;
/**
* Configuration for customizing the registration of the {@link SessionRepositoryFilter}.
*
* @author Andy Wilkinson
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(SessionRepositoryFilter.class)
@EnableConfigurationProperties(SessionProperties.class)
class SessionRepositoryFilterConfiguration {
@Bean
DelegatingFilterProxyRegistrationBean sessionRepositoryFilterRegistration(SessionProperties sessionProperties,
ListableBeanFactory beanFactory) {
String[] targetBeanNames = beanFactory.getBeanNamesForType(SessionRepositoryFilter.class, false, false);
Assert.state(targetBeanNames.length == 1, "Expected single SessionRepositoryFilter bean");
DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean(
targetBeanNames[0]);
registration.setDispatcherTypes(getDispatcherTypes(sessionProperties));
registration.setOrder(sessionProperties.getServlet().getFilterOrder());
return registration;
}
private EnumSet<DispatcherType> getDispatcherTypes(SessionProperties sessionProperties) {
SessionProperties.Servlet servletProperties = sessionProperties.getServlet();
if (servletProperties.getFilterDispatcherTypes() == null) {
return null;
}
return servletProperties.getFilterDispatcherTypes()
.stream()
.map((type) -> DispatcherType.valueOf(type.name()))
.collect(Collectors.toCollection(() -> EnumSet.noneOf(DispatcherType.class)));
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2019 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.
*/
package org.springframework.boot.session.autoconfigure;

View File

@@ -0,0 +1,12 @@
{
"properties": [
{
"name": "spring.session.servlet.filter-dispatcher-types",
"defaultValue": [
"async",
"error",
"request"
]
}
]
}

View File

@@ -0,0 +1 @@
org.springframework.boot.session.autoconfigure.SessionAutoConfiguration

View File

@@ -0,0 +1,66 @@
/*
* 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.autoconfigure;
import java.util.LinkedHashMap;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration;
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
import org.springframework.util.Assert;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests to ensure {@link SessionAutoConfiguration} and
* {@link SessionRepositoryFilterConfiguration} does not cause early initialization.
*
* @author Phillip Webb
*/
class SessionAutoConfigurationEarlyInitializationIntegrationTests {
@Test
void configurationIsFrozenWhenSessionRepositoryAccessed() {
new WebApplicationContextRunner(AnnotationConfigServletWebServerApplicationContext::new)
.withSystemProperties("spring.jndi.ignore=true")
.withPropertyValues("server.port=0")
.withUserConfiguration(TestConfiguration.class)
.run((context) -> assertThat(context).hasSingleBean(MapSessionRepository.class));
}
@Configuration(proxyBeanMethods = false)
@EnableSpringHttpSession
@ImportAutoConfiguration({ TomcatServletWebServerAutoConfiguration.class, SessionAutoConfiguration.class })
static class TestConfiguration {
@Bean
MapSessionRepository mapSessionRepository(ConfigurableApplicationContext context) {
Assert.isTrue(context.getBeanFactory().isConfigurationFrozen(), "'context' should be frozen");
return new MapSessionRepository(new LinkedHashMap<>());
}
}
}

View File

@@ -0,0 +1,295 @@
/*
* 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.autoconfigure;
import java.util.Collections;
import jakarta.servlet.DispatcherType;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.web.servlet.AbstractFilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
import org.springframework.session.security.web.authentication.SpringSessionRememberMeServices;
import org.springframework.session.web.http.CookieHttpSessionIdResolver;
import org.springframework.session.web.http.DefaultCookieSerializer;
import org.springframework.session.web.http.HeaderHttpSessionIdResolver;
import org.springframework.session.web.http.HttpSessionIdResolver;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.web.filter.DelegatingFilterProxy;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link SessionAutoConfiguration}.
*
* @author Dave Syer
* @author Eddú Meléndez
* @author Stephane Nicoll
* @author Vedran Pavic
*/
class SessionAutoConfigurationTests {
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(SessionAutoConfiguration.class));
@Test
void filterIsRegisteredWithAsyncErrorAndRequestDispatcherTypes() {
this.contextRunner.withUserConfiguration(SessionRepositoryConfiguration.class).run((context) -> {
AbstractFilterRegistrationBean<?> registration = context.getBean(AbstractFilterRegistrationBean.class);
DelegatingFilterProxy delegatingFilterProxy = (DelegatingFilterProxy) registration.getFilter();
try {
// Trigger actual initialization
delegatingFilterProxy.doFilter(null, null, null);
}
catch (Exception ex) {
// Ignore
}
assertThat(delegatingFilterProxy).extracting("delegate")
.isSameAs(context.getBean(SessionRepositoryFilter.class));
assertThat(registration)
.extracting("dispatcherTypes", InstanceOfAssertFactories.iterable(DispatcherType.class))
.containsOnly(DispatcherType.ASYNC, DispatcherType.ERROR, DispatcherType.REQUEST);
});
}
@Test
void filterOrderCanBeCustomizedWithCustomStore() {
this.contextRunner.withUserConfiguration(SessionRepositoryConfiguration.class)
.withPropertyValues("spring.session.servlet.filter-order=123")
.run((context) -> {
AbstractFilterRegistrationBean<?> registration = context.getBean(AbstractFilterRegistrationBean.class);
assertThat(registration.getOrder()).isEqualTo(123);
});
}
@Test
void filterDispatcherTypesCanBeCustomized() {
this.contextRunner.withUserConfiguration(SessionRepositoryConfiguration.class)
.withPropertyValues("spring.session.servlet.filter-dispatcher-types=error, request")
.run((context) -> {
AbstractFilterRegistrationBean<?> registration = context.getBean(AbstractFilterRegistrationBean.class);
assertThat(registration)
.extracting("dispatcherTypes", InstanceOfAssertFactories.iterable(DispatcherType.class))
.containsOnly(DispatcherType.ERROR, DispatcherType.REQUEST);
});
}
@Test
void emptyFilterDispatcherTypesDoNotThrowException() {
this.contextRunner.withUserConfiguration(SessionRepositoryConfiguration.class)
.withPropertyValues("spring.session.servlet.filter-dispatcher-types=")
.run((context) -> {
AbstractFilterRegistrationBean<?> registration = context.getBean(AbstractFilterRegistrationBean.class);
assertThat(registration)
.extracting("dispatcherTypes", InstanceOfAssertFactories.iterable(DispatcherType.class))
.isEmpty();
});
}
@Test
void sessionCookieConfigurationIsAppliedToAutoConfiguredCookieSerializer() {
this.contextRunner.withUserConfiguration(SessionRepositoryConfiguration.class)
.withPropertyValues("server.servlet.session.cookie.name=sid", "server.servlet.session.cookie.domain=spring",
"server.servlet.session.cookie.path=/test", "server.servlet.session.cookie.httpOnly=false",
"server.servlet.session.cookie.secure=false", "server.servlet.session.cookie.maxAge=10s",
"server.servlet.session.cookie.sameSite=strict", "server.servlet.session.cookie.partitioned=true")
.run((context) -> {
DefaultCookieSerializer cookieSerializer = context.getBean(DefaultCookieSerializer.class);
assertThat(cookieSerializer).hasFieldOrPropertyWithValue("cookieName", "sid");
assertThat(cookieSerializer).hasFieldOrPropertyWithValue("domainName", "spring");
assertThat(cookieSerializer).hasFieldOrPropertyWithValue("cookiePath", "/test");
assertThat(cookieSerializer).hasFieldOrPropertyWithValue("useHttpOnlyCookie", false);
assertThat(cookieSerializer).hasFieldOrPropertyWithValue("useSecureCookie", false);
assertThat(cookieSerializer).hasFieldOrPropertyWithValue("cookieMaxAge", 10);
assertThat(cookieSerializer).hasFieldOrPropertyWithValue("sameSite", "Strict");
assertThat(cookieSerializer).hasFieldOrPropertyWithValue("partitioned", true);
});
}
@Test
void sessionCookieSameSiteOmittedIsAppliedToAutoConfiguredCookieSerializer() {
this.contextRunner.withUserConfiguration(SessionRepositoryConfiguration.class)
.withPropertyValues("server.servlet.session.cookie.sameSite=omitted")
.run((context) -> {
DefaultCookieSerializer cookieSerializer = context.getBean(DefaultCookieSerializer.class);
assertThat(cookieSerializer).hasFieldOrPropertyWithValue("sameSite", null);
});
}
@Test
void autoConfiguredCookieSerializerIsUsedBySessionRepositoryFilter() {
this.contextRunner.withUserConfiguration(SessionRepositoryConfiguration.class)
.withPropertyValues("server.port=0")
.run((context) -> {
SessionRepositoryFilter<?> filter = context.getBean(SessionRepositoryFilter.class);
assertThat(filter).extracting("httpSessionIdResolver.cookieSerializer")
.isSameAs(context.getBean(DefaultCookieSerializer.class));
});
}
@Test
void autoConfiguredCookieSerializerBacksOffWhenUserConfiguresACookieSerializer() {
this.contextRunner.withUserConfiguration(UserProvidedCookieSerializerConfiguration.class).run((context) -> {
assertThat(context).hasSingleBean(DefaultCookieSerializer.class);
assertThat(context).hasBean("myCookieSerializer");
});
}
@Test
void cookiesSerializerIsAutoConfiguredWhenUserConfiguresCookieHttpSessionIdResolver() {
this.contextRunner.withUserConfiguration(UserProvidedCookieHttpSessionStrategyConfiguration.class)
.run((context) -> assertThat(context.getBeansOfType(DefaultCookieSerializer.class)).isNotEmpty());
}
@Test
void autoConfiguredCookieSerializerBacksOffWhenUserConfiguresHeaderHttpSessionIdResolver() {
this.contextRunner.withUserConfiguration(UserProvidedHeaderHttpSessionStrategyConfiguration.class)
.run((context) -> assertThat(context.getBeansOfType(DefaultCookieSerializer.class)).isEmpty());
}
@Test
void autoConfiguredCookieSerializerBacksOffWhenUserConfiguresCustomHttpSessionIdResolver() {
this.contextRunner.withUserConfiguration(UserProvidedCustomHttpSessionStrategyConfiguration.class)
.run((context) -> assertThat(context.getBeansOfType(DefaultCookieSerializer.class)).isEmpty());
}
@Test
void autoConfiguredCookieSerializerIsConfiguredWithRememberMeRequestAttribute() {
this.contextRunner.withBean(SpringSessionRememberMeServicesConfiguration.class).run((context) -> {
DefaultCookieSerializer cookieSerializer = context.getBean(DefaultCookieSerializer.class);
assertThat(cookieSerializer).hasFieldOrPropertyWithValue("rememberMeRequestAttribute",
SpringSessionRememberMeServices.REMEMBER_ME_LOGIN_ATTR);
});
}
@Test
void cookieSerializerCustomization() {
this.contextRunner.withBean(CookieSerializerCustomization.class).run((context) -> {
CookieSerializerCustomization customization = context.getBean(CookieSerializerCustomization.class);
InOrder inOrder = inOrder(customization.customizer1, customization.customizer2);
inOrder.verify(customization.customizer1).customize(any());
inOrder.verify(customization.customizer2).customize(any());
});
}
@Configuration(proxyBeanMethods = false)
@EnableSpringHttpSession
static class SessionRepositoryConfiguration {
@Bean
MapSessionRepository mySessionRepository() {
return new MapSessionRepository(Collections.emptyMap());
}
}
@EnableConfigurationProperties(ServerProperties.class)
static class ServerPropertiesConfiguration {
}
@Configuration(proxyBeanMethods = false)
@EnableSpringHttpSession
static class UserProvidedCookieSerializerConfiguration extends SessionRepositoryConfiguration {
@Bean
DefaultCookieSerializer myCookieSerializer() {
return new DefaultCookieSerializer();
}
}
@Configuration(proxyBeanMethods = false)
@EnableSpringHttpSession
static class UserProvidedCookieHttpSessionStrategyConfiguration extends SessionRepositoryConfiguration {
@Bean
CookieHttpSessionIdResolver httpSessionStrategy() {
return new CookieHttpSessionIdResolver();
}
}
@Configuration(proxyBeanMethods = false)
@EnableSpringHttpSession
static class UserProvidedHeaderHttpSessionStrategyConfiguration extends SessionRepositoryConfiguration {
@Bean
HeaderHttpSessionIdResolver httpSessionStrategy() {
return HeaderHttpSessionIdResolver.xAuthToken();
}
}
@Configuration(proxyBeanMethods = false)
@EnableSpringHttpSession
static class UserProvidedCustomHttpSessionStrategyConfiguration extends SessionRepositoryConfiguration {
@Bean
HttpSessionIdResolver httpSessionStrategy() {
return mock(HttpSessionIdResolver.class);
}
}
@Configuration(proxyBeanMethods = false)
@EnableSpringHttpSession
static class SpringSessionRememberMeServicesConfiguration extends SessionRepositoryConfiguration {
@Bean
SpringSessionRememberMeServices rememberMeServices() {
return new SpringSessionRememberMeServices();
}
}
@Configuration(proxyBeanMethods = false)
@EnableSpringHttpSession
static class CookieSerializerCustomization extends SessionRepositoryConfiguration {
private final DefaultCookieSerializerCustomizer customizer1 = mock(DefaultCookieSerializerCustomizer.class);
private final DefaultCookieSerializerCustomizer customizer2 = mock(DefaultCookieSerializerCustomizer.class);
@Bean
@Order(1)
DefaultCookieSerializerCustomizer customizer1() {
return this.customizer1;
}
@Bean
@Order(2)
DefaultCookieSerializerCustomizer customizer2() {
return this.customizer2;
}
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.session.autoconfigure;
import java.util.Collections;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
import org.springframework.session.web.http.DefaultCookieSerializer;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link SessionAutoConfiguration} when Spring Security is not on the
* classpath.
*
* @author Vedran Pavic
*/
@ClassPathExclusions("spring-security-*")
class SessionAutoConfigurationWithoutSecurityTests {
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(SessionAutoConfiguration.class));
@Test
void sessionCookieConfigurationIsAppliedToAutoConfiguredCookieSerializer() {
this.contextRunner.withUserConfiguration(SessionRepositoryConfiguration.class).run((context) -> {
DefaultCookieSerializer cookieSerializer = context.getBean(DefaultCookieSerializer.class);
assertThat(cookieSerializer).hasFieldOrPropertyWithValue("rememberMeRequestAttribute", null);
});
}
@Configuration(proxyBeanMethods = false)
@EnableSpringHttpSession
static class SessionRepositoryConfiguration {
@Bean
MapSessionRepository mySessionRepository() {
return new MapSessionRepository(Collections.emptyMap());
}
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.session.autoconfigure;
import java.time.Duration;
import java.util.function.Supplier;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link SessionProperties}.
*
* @author Stephane Nicoll
*/
class SessionPropertiesTests {
@Test
@SuppressWarnings("unchecked")
void determineTimeoutWithTimeoutIgnoreFallback() {
SessionProperties properties = new SessionProperties();
properties.setTimeout(Duration.ofMinutes(1));
Supplier<Duration> fallback = mock(Supplier.class);
assertThat(properties.determineTimeout(fallback)).isEqualTo(Duration.ofMinutes(1));
then(fallback).shouldHaveNoInteractions();
}
@Test
void determineTimeoutWithNoTimeoutUseFallback() {
SessionProperties properties = new SessionProperties();
properties.setTimeout(null);
Duration fallback = Duration.ofMinutes(2);
assertThat(properties.determineTimeout(() -> fallback)).isSameAs(fallback);
}
}

View File

@@ -0,0 +1,105 @@
/*
* 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.autoconfigure;
import java.util.Collections;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
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.web.reactive.context.ReactiveWebApplicationContext;
import org.springframework.boot.web.server.reactive.MockReactiveWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.session.ReactiveMapSessionRepository;
import org.springframework.session.ReactiveSessionRepository;
import org.springframework.session.config.annotation.web.server.EnableSpringWebSession;
import org.springframework.web.server.WebSession;
import org.springframework.web.server.session.WebSessionManager;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Base class for Spring Session auto-configuration tests when the backing store is
* reactive.
*
* @author Andy Wilkinson
*/
public abstract class AbstractReactiveSessionAutoConfigurationTests {
private static final MockReactiveWebServerFactory mockReactiveWebServerFactory = new MockReactiveWebServerFactory();
protected ReactiveWebApplicationContextRunner contextRunner;
@Test
void backOffIfReactiveSessionRepositoryIsPresent() {
this.contextRunner.withUserConfiguration(ReactiveSessionRepositoryConfiguration.class).run((context) -> {
ReactiveMapSessionRepository repository = validateSessionRepository(context,
ReactiveMapSessionRepository.class);
assertThat(context).getBean("mySessionRepository").isSameAs(repository);
});
}
protected ContextConsumer<ReactiveWebApplicationContext> assertExchangeWithSession(
Consumer<MockServerWebExchange> exchange) {
return (context) -> {
MockServerHttpRequest request = MockServerHttpRequest.get("/").build();
MockServerWebExchange webExchange = MockServerWebExchange.from(request);
WebSessionManager webSessionManager = context.getBean(WebSessionManager.class);
WebSession webSession = webSessionManager.getSession(webExchange).block();
webSession.start();
webExchange.getResponse().setComplete().block();
exchange.accept(webExchange);
};
}
protected <T extends ReactiveSessionRepository<?>> T validateSessionRepository(
AssertableReactiveWebApplicationContext context, Class<T> type) {
assertThat(context).hasSingleBean(WebSessionManager.class);
assertThat(context).hasSingleBean(ReactiveSessionRepository.class);
ReactiveSessionRepository<?> repository = context.getBean(ReactiveSessionRepository.class);
assertThat(repository).as("Wrong session repository type").isInstanceOf(type);
return type.cast(repository);
}
@Configuration(proxyBeanMethods = false)
protected static class ReactiveWebServerConfiguration {
@Bean
MockReactiveWebServerFactory mockReactiveWebServerFactory() {
return mockReactiveWebServerFactory;
}
}
@Configuration(proxyBeanMethods = false)
@EnableSpringWebSession
static class ReactiveSessionRepositoryConfiguration {
@Bean
ReactiveMapSessionRepository mySessionRepository() {
return new ReactiveMapSessionRepository(Collections.emptyMap());
}
}
}

View File

@@ -0,0 +1,74 @@
/*
* 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.autoconfigure;
import java.util.Collections;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.SessionRepository;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
import org.springframework.session.web.http.SessionRepositoryFilter;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Base class for Spring Session auto-configuration tests when the backing store cannot be
* reactive.
*
* @author Stephane Nicoll
* @author Weix Sun
* @see AbstractReactiveSessionAutoConfigurationTests
*/
public abstract class AbstractSessionAutoConfigurationTests {
protected WebApplicationContextRunner contextRunner;
@Test
void backOffIfSessionRepositoryIsPresent() {
this.contextRunner.withUserConfiguration(SessionRepositoryConfiguration.class).run((context) -> {
MapSessionRepository repository = validateSessionRepository(context, MapSessionRepository.class);
assertThat(context).getBean("mySessionRepository").isSameAs(repository);
});
}
protected <T extends SessionRepository<?>> T validateSessionRepository(AssertableWebApplicationContext context,
Class<T> type) {
assertThat(context).hasSingleBean(SessionRepositoryFilter.class);
assertThat(context).hasSingleBean(SessionRepository.class);
SessionRepository<?> repository = context.getBean(SessionRepository.class);
assertThat(repository).as("Wrong session repository type").isInstanceOf(type);
return type.cast(repository);
}
@Configuration
@EnableSpringHttpSession
static class SessionRepositoryConfiguration {
@Bean
MapSessionRepository mySessionRepository() {
return new MapSessionRepository(Collections.emptyMap());
}
}
}