diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java
index 53af9b6e1e..2688793d08 100644
--- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java
+++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java
@@ -530,6 +530,66 @@ public final class HttpSecurity extends
return getOrApply(new SessionManagementConfigurer<>());
}
+ /**
+ * Allows configuring of Session Management.
+ *
+ *
Example Configuration
+ *
+ * The following configuration demonstrates how to enforce that only a single instance
+ * of a user is authenticated at a time. If a user authenticates with the username
+ * "user" without logging out and an attempt to authenticate with "user" is made the
+ * first session will be forcibly terminated and sent to the "/login?expired" URL.
+ *
+ *
+ * @Configuration
+ * @EnableWebSecurity
+ * public class SessionManagementSecurityConfig extends WebSecurityConfigurerAdapter {
+ *
+ * @Override
+ * protected void configure(HttpSecurity http) throws Exception {
+ * http
+ * .authorizeRequests()
+ * .anyRequest().hasRole("USER")
+ * .and()
+ * .formLogin(formLogin ->
+ * formLogin
+ * .permitAll()
+ * )
+ * .sessionManagement(sessionManagement ->
+ * sessionManagement
+ * .maximumSessions(1)
+ * .expiredUrl("/login?expired")
+ * );
+ * }
+ * }
+ *
+ *
+ * When using {@link SessionManagementConfigurer#maximumSessions(int)}, do not forget
+ * to configure {@link HttpSessionEventPublisher} for the application to ensure that
+ * expired sessions are cleaned up.
+ *
+ * In a web.xml this can be configured using the following:
+ *
+ *
+ * <listener>
+ * <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
+ * </listener>
+ *
+ *
+ * Alternatively,
+ * {@link AbstractSecurityWebApplicationInitializer#enableHttpSessionEventPublisher()}
+ * could return true.
+ *
+ * @param sessionManagementCustomizer the {@link Customizer} to provide more options for
+ * the {@link SessionManagementConfigurer}
+ * @return the {@link HttpSecurity} for further customizations
+ * @throws Exception
+ */
+ public HttpSecurity sessionManagement(Customizer> sessionManagementCustomizer) throws Exception {
+ sessionManagementCustomizer.customize(getOrApply(new SessionManagementConfigurer<>()));
+ return HttpSecurity.this;
+ }
+
/**
* Allows configuring a {@link PortMapper} that is available from
* {@link HttpSecurity#getSharedObject(Class)}. Other provided
diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java
index 4278811084..b737422811 100644
--- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java
+++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java
@@ -54,6 +54,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import static org.springframework.security.config.Customizer.withDefaults;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@@ -262,6 +263,73 @@ public class SessionManagementConfigurerTests {
}
}
+ @Test
+ public void loginWhenUserLoggedInAndMaxSessionsOneInLambdaThenLoginPrevented() throws Exception {
+ this.spring.register(ConcurrencyControlInLambdaConfig.class).autowire();
+
+ this.mvc.perform(post("/login")
+ .with(csrf())
+ .param("username", "user")
+ .param("password", "password"));
+
+ this.mvc.perform(post("/login")
+ .with(csrf())
+ .param("username", "user")
+ .param("password", "password"))
+ .andExpect(status().isFound())
+ .andExpect(redirectedUrl("/login?error"));
+ }
+
+ @EnableWebSecurity
+ static class ConcurrencyControlInLambdaConfig extends WebSecurityConfigurerAdapter {
+ @Override
+ public void configure(HttpSecurity http) throws Exception {
+ // @formatter:off
+ http
+ .formLogin(withDefaults())
+ .sessionManagement(sessionManagement ->
+ sessionManagement
+ .maximumSessions(1)
+ .maxSessionsPreventsLogin(true)
+ );
+ // @formatter:on
+ }
+
+ @Override
+ protected void configure(AuthenticationManagerBuilder auth) throws Exception {
+ // @formatter:off
+ auth
+ .inMemoryAuthentication()
+ .withUser(PasswordEncodedUser.user());
+ // @formatter:on
+ }
+ }
+
+ @Test
+ public void requestWhenSessionCreationPolicyStateLessInLambdaThenNoSessionCreated() throws Exception {
+ this.spring.register(SessionCreationPolicyStateLessInLambdaConfig.class).autowire();
+
+ MvcResult mvcResult = this.mvc.perform(get("/"))
+ .andReturn();
+ HttpSession session = mvcResult.getRequest().getSession(false);
+
+ assertThat(session).isNull();
+ }
+
+ @EnableWebSecurity
+ static class SessionCreationPolicyStateLessInLambdaConfig extends WebSecurityConfigurerAdapter {
+ @Override
+ protected void configure(HttpSecurity http) throws Exception {
+ // @formatter:off
+ http
+ .sessionManagement(sessionManagement ->
+ sessionManagement
+ .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+ );
+ // @formatter:on
+ }
+ }
+
@Test
public void configureWhenRegisteringObjectPostProcessorThenInvokedOnSessionManagementFilter() {
ObjectPostProcessorConfig.objectPostProcessor = spy(ReflectingObjectPostProcessor.class);