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);