浏览代码

Address SessionLimitStrategy

Closes gh-16206
Claudenir Machado 8 月之前
父节点
当前提交
1864577e98
共有 14 个文件被更改,包括 627 次插入15 次删除
  1. 18 5
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java
  2. 17 1
      config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java
  3. 3 0
      config/src/main/resources/org/springframework/security/config/spring-security-6.5.rnc
  4. 7 0
      config/src/main/resources/org/springframework/security/config/spring-security-6.5.xsd
  5. 114 1
      config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java
  6. 136 1
      config/src/test/java/org/springframework/security/config/http/HttpHeadersConfigTests.java
  7. 38 0
      config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsSessionManagementConcurrencyControlMaxSessions.xml
  8. 41 0
      config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsSessionManagementConcurrencyControlMaxSessionsRef.xml
  9. 42 0
      config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsSessionManagementConcurrencyControlWithInvalidMaxSessionsConfig.xml
  10. 3 0
      docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc
  11. 16 6
      web/src/main/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategy.java
  12. 49 0
      web/src/main/java/org/springframework/security/web/session/SessionLimit.java
  13. 84 1
      web/src/test/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategyTests.java
  14. 59 0
      web/src/test/java/org/springframework/security/web/session/SessionLimitTests.java

+ 18 - 5
config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java

@@ -59,6 +59,7 @@ import org.springframework.security.web.session.DisableEncodeUrlFilter;
 import org.springframework.security.web.session.ForceEagerSessionCreationFilter;
 import org.springframework.security.web.session.InvalidSessionStrategy;
 import org.springframework.security.web.session.SessionInformationExpiredStrategy;
+import org.springframework.security.web.session.SessionLimit;
 import org.springframework.security.web.session.SessionManagementFilter;
 import org.springframework.security.web.session.SimpleRedirectInvalidSessionStrategy;
 import org.springframework.security.web.session.SimpleRedirectSessionInformationExpiredStrategy;
@@ -123,7 +124,7 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
 
 	private SessionRegistry sessionRegistry;
 
-	private Integer maximumSessions;
+	private SessionLimit sessionLimit;
 
 	private String expiredUrl;
 
@@ -329,7 +330,7 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
 	 * @return the {@link SessionManagementConfigurer} for further customizations
 	 */
 	public ConcurrencyControlConfigurer maximumSessions(int maximumSessions) {
-		this.maximumSessions = maximumSessions;
+		this.sessionLimit = SessionLimit.of(maximumSessions);
 		this.propertiesThatRequireImplicitAuthentication.add("maximumSessions = " + maximumSessions);
 		return new ConcurrencyControlConfigurer();
 	}
@@ -570,7 +571,7 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
 			SessionRegistry sessionRegistry = getSessionRegistry(http);
 			ConcurrentSessionControlAuthenticationStrategy concurrentSessionControlStrategy = new ConcurrentSessionControlAuthenticationStrategy(
 					sessionRegistry);
-			concurrentSessionControlStrategy.setMaximumSessions(this.maximumSessions);
+			concurrentSessionControlStrategy.setMaximumSessions(this.sessionLimit);
 			concurrentSessionControlStrategy.setExceptionIfMaximumExceeded(this.maxSessionsPreventsLogin);
 			concurrentSessionControlStrategy = postProcess(concurrentSessionControlStrategy);
 			RegisterSessionAuthenticationStrategy registerSessionStrategy = new RegisterSessionAuthenticationStrategy(
@@ -614,7 +615,7 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
 	 * @return
 	 */
 	private boolean isConcurrentSessionControlEnabled() {
-		return this.maximumSessions != null;
+		return this.sessionLimit != null;
 	}
 
 	/**
@@ -706,7 +707,19 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
 		 * @return the {@link ConcurrencyControlConfigurer} for further customizations
 		 */
 		public ConcurrencyControlConfigurer maximumSessions(int maximumSessions) {
-			SessionManagementConfigurer.this.maximumSessions = maximumSessions;
+			SessionManagementConfigurer.this.sessionLimit = SessionLimit.of(maximumSessions);
+			return this;
+		}
+
+		/**
+		 * Determines the behaviour when a session limit is detected.
+		 * @param sessionLimit the {@link SessionLimit} to check the maximum number of
+		 * sessions for a user
+		 * @return the {@link ConcurrencyControlConfigurer} for further customizations
+		 * @since 6.5
+		 */
+		public ConcurrencyControlConfigurer maximumSessions(SessionLimit sessionLimit) {
+			SessionManagementConfigurer.this.sessionLimit = sessionLimit;
 			return this;
 		}
 

+ 17 - 1
config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java

@@ -122,6 +122,10 @@ class HttpConfigurationBuilder {
 
 	private static final String ATT_SESSION_AUTH_STRATEGY_REF = "session-authentication-strategy-ref";
 
+	private static final String ATT_MAX_SESSIONS_REF = "max-sessions-ref";
+
+	private static final String ATT_MAX_SESSIONS = "max-sessions";
+
 	private static final String ATT_SESSION_AUTH_ERROR_URL = "session-authentication-error-url";
 
 	private static final String ATT_SECURITY_CONTEXT_HOLDER_STRATEGY = "security-context-holder-strategy-ref";
@@ -485,10 +489,16 @@ class HttpConfigurationBuilder {
 			concurrentSessionStrategy.addConstructorArgValue(this.sessionRegistryRef);
 			String maxSessions = this.pc.getReaderContext()
 				.getEnvironment()
-				.resolvePlaceholders(sessionCtrlElt.getAttribute("max-sessions"));
+				.resolvePlaceholders(sessionCtrlElt.getAttribute(ATT_MAX_SESSIONS));
 			if (StringUtils.hasText(maxSessions)) {
 				concurrentSessionStrategy.addPropertyValue("maximumSessions", maxSessions);
 			}
+			String maxSessionsRef = this.pc.getReaderContext()
+				.getEnvironment()
+				.resolvePlaceholders(sessionCtrlElt.getAttribute(ATT_MAX_SESSIONS_REF));
+			if (StringUtils.hasText(maxSessionsRef)) {
+				concurrentSessionStrategy.addPropertyReference("maximumSessions", maxSessionsRef);
+			}
 			String exceptionIfMaximumExceeded = sessionCtrlElt.getAttribute("error-if-maximum-exceeded");
 			if (StringUtils.hasText(exceptionIfMaximumExceeded)) {
 				concurrentSessionStrategy.addPropertyValue("exceptionIfMaximumExceeded", exceptionIfMaximumExceeded);
@@ -591,6 +601,12 @@ class HttpConfigurationBuilder {
 				.error("Cannot use 'expired-url' attribute and 'expired-session-strategy-ref'" + " attribute together.",
 						source);
 		}
+		String maxSessions = element.getAttribute(ATT_MAX_SESSIONS);
+		String maxSessionsRef = element.getAttribute(ATT_MAX_SESSIONS_REF);
+		if (StringUtils.hasText(maxSessions) && StringUtils.hasText(maxSessionsRef)) {
+			this.pc.getReaderContext()
+				.error("Cannot use 'max-sessions' attribute and 'max-sessions-ref' attribute together.", source);
+		}
 		if (StringUtils.hasText(expiryUrl)) {
 			BeanDefinitionBuilder expiredSessionBldr = BeanDefinitionBuilder
 				.rootBeanDefinition(SimpleRedirectSessionInformationExpiredStrategy.class);

+ 3 - 0
config/src/main/resources/org/springframework/security/config/spring-security-6.5.rnc

@@ -934,6 +934,9 @@ concurrency-control =
 concurrency-control.attlist &=
 	## The maximum number of sessions a single authenticated user can have open at the same time. Defaults to "1". A negative value denotes unlimited sessions.
 	attribute max-sessions {xsd:token}?
+concurrency-control.attlist &=
+	## Allows injection of the SessionLimit instance used by the ConcurrentSessionControlAuthenticationStrategy
+	attribute max-sessions-ref {xsd:token}?
 concurrency-control.attlist &=
 	## The URL a user will be redirected to if they attempt to use a session which has been "expired" because they have logged in again.
 	attribute expired-url {xsd:token}?

+ 7 - 0
config/src/main/resources/org/springframework/security/config/spring-security-6.5.xsd

@@ -2688,6 +2688,13 @@
                 </xs:documentation>
          </xs:annotation>
       </xs:attribute>
+      <xs:attribute name="max-sessions-ref" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>Allows injection of the SessionLimit instance used by the
+                ConcurrentSessionControlAuthenticationStrategy
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
       <xs:attribute name="expired-url" type="xs:token">
          <xs:annotation>
             <xs:documentation>The URL a user will be redirected to if they attempt to use a session which has been

+ 114 - 1
config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2024 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.
@@ -64,6 +64,7 @@ import org.springframework.security.web.context.SecurityContextRepository;
 import org.springframework.security.web.savedrequest.RequestCache;
 import org.springframework.security.web.session.ConcurrentSessionFilter;
 import org.springframework.security.web.session.HttpSessionDestroyedEvent;
+import org.springframework.security.web.session.SessionLimit;
 import org.springframework.security.web.session.SessionManagementFilter;
 import org.springframework.test.web.servlet.MockMvc;
 import org.springframework.test.web.servlet.MvcResult;
@@ -249,6 +250,82 @@ public class SessionManagementConfigurerTests {
 		// @formatter:on
 	}
 
+	@Test
+	public void loginWhenAdminUserLoggedInAndSessionLimitIsConfiguredThenLoginSuccessfully() throws Exception {
+		this.spring.register(ConcurrencyControlWithSessionLimitConfig.class).autowire();
+		// @formatter:off
+		MockHttpServletRequestBuilder requestBuilder = post("/login")
+				.with(csrf())
+				.param("username", "admin")
+				.param("password", "password");
+		HttpSession firstSession = this.mvc.perform(requestBuilder)
+				.andExpect(status().is3xxRedirection())
+				.andExpect(redirectedUrl("/"))
+				.andReturn()
+				.getRequest()
+				.getSession(false);
+		assertThat(firstSession).isNotNull();
+		HttpSession secondSession = this.mvc.perform(requestBuilder)
+				.andExpect(status().is3xxRedirection())
+				.andExpect(redirectedUrl("/"))
+				.andReturn()
+				.getRequest()
+				.getSession(false);
+		assertThat(secondSession).isNotNull();
+		// @formatter:on
+		assertThat(firstSession.getId()).isNotEqualTo(secondSession.getId());
+	}
+
+	@Test
+	public void loginWhenAdminUserLoggedInAndSessionLimitIsConfiguredThenLoginPrevented() throws Exception {
+		this.spring.register(ConcurrencyControlWithSessionLimitConfig.class).autowire();
+		// @formatter:off
+		MockHttpServletRequestBuilder requestBuilder = post("/login")
+				.with(csrf())
+				.param("username", "admin")
+				.param("password", "password");
+		HttpSession firstSession = this.mvc.perform(requestBuilder)
+				.andExpect(status().is3xxRedirection())
+				.andExpect(redirectedUrl("/"))
+				.andReturn()
+				.getRequest()
+				.getSession(false);
+		assertThat(firstSession).isNotNull();
+		HttpSession secondSession = this.mvc.perform(requestBuilder)
+				.andExpect(status().is3xxRedirection())
+				.andExpect(redirectedUrl("/"))
+				.andReturn()
+				.getRequest()
+				.getSession(false);
+		assertThat(secondSession).isNotNull();
+		assertThat(firstSession.getId()).isNotEqualTo(secondSession.getId());
+		this.mvc.perform(requestBuilder)
+				.andExpect(status().isFound())
+				.andExpect(redirectedUrl("/login?error"));
+		// @formatter:on
+	}
+
+	@Test
+	public void loginWhenUserLoggedInAndSessionLimitIsConfiguredThenLoginPrevented() throws Exception {
+		this.spring.register(ConcurrencyControlWithSessionLimitConfig.class).autowire();
+		// @formatter:off
+		MockHttpServletRequestBuilder requestBuilder = post("/login")
+				.with(csrf())
+				.param("username", "user")
+				.param("password", "password");
+		HttpSession firstSession = this.mvc.perform(requestBuilder)
+				.andExpect(status().is3xxRedirection())
+				.andExpect(redirectedUrl("/"))
+				.andReturn()
+				.getRequest()
+				.getSession(false);
+		assertThat(firstSession).isNotNull();
+		this.mvc.perform(requestBuilder)
+				.andExpect(status().isFound())
+				.andExpect(redirectedUrl("/login?error"));
+		// @formatter:on
+	}
+
 	@Test
 	public void requestWhenSessionCreationPolicyStateLessInLambdaThenNoSessionCreated() throws Exception {
 		this.spring.register(SessionCreationPolicyStateLessInLambdaConfig.class).autowire();
@@ -625,6 +702,42 @@ public class SessionManagementConfigurerTests {
 
 	}
 
+	@Configuration
+	@EnableWebSecurity
+	static class ConcurrencyControlWithSessionLimitConfig {
+
+		@Bean
+		SecurityFilterChain filterChain(HttpSecurity http, SessionLimit sessionLimit) throws Exception {
+			// @formatter:off
+			http
+					.formLogin(withDefaults())
+					.sessionManagement((sessionManagement) -> sessionManagement
+									.sessionConcurrency((sessionConcurrency) -> sessionConcurrency
+													.maximumSessions(sessionLimit)
+													.maxSessionsPreventsLogin(true)
+									)
+					);
+			// @formatter:on
+			return http.build();
+		}
+
+		@Bean
+		UserDetailsService userDetailsService() {
+			return new InMemoryUserDetailsManager(PasswordEncodedUser.admin(), PasswordEncodedUser.user());
+		}
+
+		@Bean
+		SessionLimit SessionLimit() {
+			return (authentication) -> {
+				if ("admin".equals(authentication.getName())) {
+					return 2;
+				}
+				return 1;
+			};
+		}
+
+	}
+
 	@Configuration
 	@EnableWebSecurity
 	static class SessionCreationPolicyStateLessInLambdaConfig {

+ 136 - 1
config/src/test/java/org/springframework/security/config/http/HttpHeadersConfigTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2024 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.
@@ -24,6 +24,7 @@ import java.util.Map;
 import java.util.Set;
 
 import com.google.common.collect.ImmutableMap;
+import jakarta.servlet.http.HttpSession;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 
@@ -33,14 +34,21 @@ import org.springframework.beans.factory.parsing.BeanDefinitionParsingException;
 import org.springframework.beans.factory.xml.XmlBeanDefinitionStoreException;
 import org.springframework.security.config.test.SpringTestContext;
 import org.springframework.security.config.test.SpringTestContextExtension;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.session.SessionLimit;
 import org.springframework.test.web.servlet.MockMvc;
 import org.springframework.test.web.servlet.ResultMatcher;
+import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RestController;
 
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 
 /**
@@ -49,6 +57,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
  * @author Josh Cummings
  * @author Rafiullah Hamedy
  * @author Marcus Da Coregio
+ * @author Claudenir Freitas
  */
 @ExtendWith(SpringTestContextExtension.class)
 public class HttpHeadersConfigTests {
@@ -782,6 +791,120 @@ public class HttpHeadersConfigTests {
 		// @formatter:on
 	}
 
+	@Test
+	public void requestWhenSessionManagementConcurrencyControlMaxSessionIsOne() throws Exception {
+		System.setProperty("security.session-management.concurrency-control.max-sessions", "1");
+		this.spring.configLocations(this.xml("DefaultsSessionManagementConcurrencyControlMaxSessions")).autowire();
+		// @formatter:off
+		MockHttpServletRequestBuilder requestBuilder = post("/login")
+				.with(csrf())
+				.param("username", "user")
+				.param("password", "password");
+		HttpSession firstSession = this.mvc.perform(requestBuilder)
+				.andExpect(status().is3xxRedirection())
+				.andExpect(redirectedUrl("/"))
+				.andReturn()
+				.getRequest()
+				.getSession(false);
+		// @formatter:on
+		assertThat(firstSession).isNotNull();
+		// @formatter:off
+		this.mvc.perform(requestBuilder)
+				.andExpect(status().isFound())
+				.andExpect(redirectedUrl("/login?error"));
+		// @formatter:on
+	}
+
+	@Test
+	public void requestWhenSessionManagementConcurrencyControlMaxSessionIsUnlimited() throws Exception {
+		System.setProperty("security.session-management.concurrency-control.max-sessions", "-1");
+		this.spring.configLocations(this.xml("DefaultsSessionManagementConcurrencyControlMaxSessions")).autowire();
+		// @formatter:off
+		MockHttpServletRequestBuilder requestBuilder = post("/login")
+				.with(csrf())
+				.param("username", "user")
+				.param("password", "password");
+		HttpSession firstSession = this.mvc.perform(requestBuilder)
+				.andExpect(status().is3xxRedirection())
+				.andExpect(redirectedUrl("/"))
+				.andReturn()
+				.getRequest()
+				.getSession(false);
+		assertThat(firstSession).isNotNull();
+		HttpSession secondSession = this.mvc.perform(requestBuilder)
+				.andExpect(status().is3xxRedirection())
+				.andExpect(redirectedUrl("/"))
+				.andReturn()
+				.getRequest()
+				.getSession(false);
+		assertThat(secondSession).isNotNull();
+		// @formatter:on
+		assertThat(firstSession.getId()).isNotEqualTo(secondSession.getId());
+	}
+
+	@Test
+	public void requestWhenSessionManagementConcurrencyControlMaxSessionRefIsOneForNonAdminUsers() throws Exception {
+		this.spring.configLocations(this.xml("DefaultsSessionManagementConcurrencyControlMaxSessionsRef")).autowire();
+		// @formatter:off
+		MockHttpServletRequestBuilder requestBuilder = post("/login")
+				.with(csrf())
+				.param("username", "user")
+				.param("password", "password");
+		HttpSession firstSession = this.mvc.perform(requestBuilder)
+				.andExpect(status().is3xxRedirection())
+				.andExpect(redirectedUrl("/"))
+				.andReturn()
+				.getRequest()
+				.getSession(false);
+		// @formatter:on
+		assertThat(firstSession).isNotNull();
+		// @formatter:off
+		this.mvc.perform(requestBuilder)
+				.andExpect(status().isFound())
+				.andExpect(redirectedUrl("/login?error"));
+		// @formatter:on
+	}
+
+	@Test
+	public void requestWhenSessionManagementConcurrencyControlMaxSessionRefIsTwoForAdminUsers() throws Exception {
+		this.spring.configLocations(this.xml("DefaultsSessionManagementConcurrencyControlMaxSessionsRef")).autowire();
+		// @formatter:off
+		MockHttpServletRequestBuilder requestBuilder = post("/login")
+				.with(csrf())
+				.param("username", "admin")
+				.param("password", "password");
+		HttpSession firstSession = this.mvc.perform(requestBuilder)
+				.andExpect(status().is3xxRedirection())
+				.andExpect(redirectedUrl("/"))
+				.andReturn()
+				.getRequest()
+				.getSession(false);
+		assertThat(firstSession).isNotNull();
+		HttpSession secondSession = this.mvc.perform(requestBuilder)
+				.andExpect(status().is3xxRedirection())
+				.andExpect(redirectedUrl("/"))
+				.andReturn()
+				.getRequest()
+				.getSession(false);
+		assertThat(secondSession).isNotNull();
+		// @formatter:on
+		assertThat(firstSession.getId()).isNotEqualTo(secondSession.getId());
+		// @formatter:off
+		this.mvc.perform(requestBuilder)
+				.andExpect(status().isFound())
+				.andExpect(redirectedUrl("/login?error"));
+		// @formatter:on
+	}
+
+	@Test
+	public void requestWhenSessionManagementConcurrencyControlWithInvalidMaxSessionConfig() {
+		assertThatExceptionOfType(BeanDefinitionParsingException.class)
+			.isThrownBy(() -> this.spring
+				.configLocations(this.xml("DefaultsSessionManagementConcurrencyControlWithInvalidMaxSessionsConfig"))
+				.autowire())
+			.withMessageContaining("Cannot use 'max-sessions' attribute and 'max-sessions-ref' attribute together.");
+	}
+
 	private static ResultMatcher includesDefaults() {
 		return includes(defaultHeaders);
 	}
@@ -832,4 +955,16 @@ public class HttpHeadersConfigTests {
 
 	}
 
+	public static class CustomSessionLimit implements SessionLimit {
+
+		@Override
+		public Integer apply(Authentication authentication) {
+			if ("admin".equals(authentication.getName())) {
+				return 2;
+			}
+			return 1;
+		}
+
+	}
+
 }

+ 38 - 0
config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsSessionManagementConcurrencyControlMaxSessions.xml

@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2002-2024 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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		xmlns="http://www.springframework.org/schema/security"
+		xsi:schemaLocation="
+			http://www.springframework.org/schema/security
+			https://www.springframework.org/schema/security/spring-security.xsd
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<http auto-config="true">
+		<session-management>
+			<concurrency-control max-sessions="${security.session-management.concurrency-control.max-sessions}"
+								 error-if-maximum-exceeded="true"/>
+		</session-management>
+		<intercept-url pattern="/**" access="permitAll"/>
+	</http>
+
+	<b:bean name="simple" class="org.springframework.security.config.http.HttpHeadersConfigTests.SimpleController"/>
+
+	<b:import resource="userservice.xml"/>
+</b:beans>

+ 41 - 0
config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsSessionManagementConcurrencyControlMaxSessionsRef.xml

@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2002-2024 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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		 xmlns="http://www.springframework.org/schema/security"
+		 xsi:schemaLocation="
+			http://www.springframework.org/schema/security
+			https://www.springframework.org/schema/security/spring-security.xsd
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<http auto-config="true">
+		<session-management>
+			<concurrency-control max-sessions-ref="customSessionLimit"
+								 error-if-maximum-exceeded="true"/>
+		</session-management>
+		<intercept-url pattern="/**" access="permitAll"/>
+	</http>
+
+	<b:bean name="simple" class="org.springframework.security.config.http.HttpHeadersConfigTests.SimpleController"/>
+
+	<b:bean name="customSessionLimit"
+			class="org.springframework.security.config.http.HttpHeadersConfigTests.CustomSessionLimit"/>
+
+	<b:import resource="userservice.xml"/>
+</b:beans>

+ 42 - 0
config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsSessionManagementConcurrencyControlWithInvalidMaxSessionsConfig.xml

@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2002-2024 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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		 xmlns="http://www.springframework.org/schema/security"
+		 xsi:schemaLocation="
+			http://www.springframework.org/schema/security
+			https://www.springframework.org/schema/security/spring-security.xsd
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<http auto-config="true">
+		<session-management>
+			<concurrency-control max-sessions="1"
+								 max-sessions-ref="customSessionLimit"
+								 error-if-maximum-exceeded="true"/>
+		</session-management>
+		<intercept-url pattern="/**" access="permitAll"/>
+	</http>
+
+	<b:bean name="simple" class="org.springframework.security.config.http.HttpHeadersConfigTests.SimpleController"/>
+
+	<b:bean name="customSessionLimit"
+			class="org.springframework.security.config.http.HttpHeadersConfigTests.CustomSessionLimit"/>
+
+	<b:import resource="userservice.xml"/>
+</b:beans>

+ 3 - 0
docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc

@@ -2168,6 +2168,9 @@ Allows injection of the ExpiredSessionStrategy instance used by the ConcurrentSe
 Maps to the `maximumSessions` property of `ConcurrentSessionControlAuthenticationStrategy`.
 Specify `-1` as the value to support unlimited sessions.
 
+[[nsa-concurrency-control-max-sessions-ref]]
+* **max-sessions-ref**
+Allows injection of the SessionLimit instance used by the ConcurrentSessionControlAuthenticationStrategy
 
 [[nsa-concurrency-control-session-registry-alias]]
 * **session-registry-alias**

+ 16 - 6
web/src/main/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategy.java

@@ -33,6 +33,7 @@ import org.springframework.security.core.session.SessionRegistry;
 import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
 import org.springframework.security.web.session.ConcurrentSessionFilter;
+import org.springframework.security.web.session.SessionLimit;
 import org.springframework.security.web.session.SessionManagementFilter;
 import org.springframework.util.Assert;
 
@@ -76,7 +77,7 @@ public class ConcurrentSessionControlAuthenticationStrategy
 
 	private boolean exceptionIfMaximumExceeded = false;
 
-	private int maximumSessions = 1;
+	private SessionLimit sessionLimit = SessionLimit.of(1);
 
 	/**
 	 * @param sessionRegistry the session registry which should be updated when the
@@ -130,7 +131,7 @@ public class ConcurrentSessionControlAuthenticationStrategy
 	 * @return either -1 meaning unlimited, or a positive integer to limit (never zero)
 	 */
 	protected int getMaximumSessionsForThisUser(Authentication authentication) {
-		return this.maximumSessions;
+		return this.sessionLimit.apply(authentication);
 	}
 
 	/**
@@ -172,15 +173,24 @@ public class ConcurrentSessionControlAuthenticationStrategy
 	}
 
 	/**
-	 * Sets the <tt>maxSessions</tt> property. The default value is 1. Use -1 for
+	 * Sets the <tt>sessionLimit</tt> property. The default value is 1. Use -1 for
 	 * unlimited sessions.
 	 * @param maximumSessions the maximum number of permitted sessions a user can have
 	 * open simultaneously.
 	 */
 	public void setMaximumSessions(int maximumSessions) {
-		Assert.isTrue(maximumSessions != 0,
-				"MaximumLogins must be either -1 to allow unlimited logins, or a positive integer to specify a maximum");
-		this.maximumSessions = maximumSessions;
+		this.sessionLimit = SessionLimit.of(maximumSessions);
+	}
+
+	/**
+	 * Sets the <tt>sessionLimit</tt> property. The default value is 1. Use -1 for
+	 * unlimited sessions.
+	 * @param sessionLimit the session limit strategy
+	 * @since 6.5
+	 */
+	public void setMaximumSessions(SessionLimit sessionLimit) {
+		Assert.notNull(sessionLimit, "sessionLimit cannot be null");
+		this.sessionLimit = sessionLimit;
 	}
 
 	/**

+ 49 - 0
web/src/main/java/org/springframework/security/web/session/SessionLimit.java

@@ -0,0 +1,49 @@
+/*
+ * Copyright 2015-2024 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.security.web.session;
+
+import java.util.function.Function;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.util.Assert;
+
+/**
+ * Represents the maximum number of sessions allowed. Use {@link #UNLIMITED} to indicate
+ * that there is no limit.
+ *
+ * @author Claudenir Freitas
+ * @since 6.5
+ */
+public interface SessionLimit extends Function<Authentication, Integer> {
+
+	/**
+	 * Represents unlimited sessions.
+	 */
+	SessionLimit UNLIMITED = (authentication) -> -1;
+
+	/**
+	 * Creates a {@link SessionLimit} that always returns the given value for any user
+	 * @param maxSessions the maximum number of sessions allowed
+	 * @return a {@link SessionLimit} instance that returns the given value.
+	 */
+	static SessionLimit of(int maxSessions) {
+		Assert.isTrue(maxSessions != 0,
+				"MaximumLogins must be either -1 to allow unlimited logins, or a positive integer to specify a maximum");
+		return (authentication) -> maxSessions;
+	}
+
+}

+ 84 - 1
web/src/test/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategyTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2024 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.
@@ -34,6 +34,7 @@ import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.session.SessionInformation;
 import org.springframework.security.core.session.SessionRegistry;
+import org.springframework.security.web.session.SessionLimit;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -41,9 +42,11 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.verifyNoInteractions;
 
 /**
  * @author Rob Winch
+ * @author Claudenir Freitas
  *
  */
 @ExtendWith(MockitoExtension.class)
@@ -144,6 +147,86 @@ public class ConcurrentSessionControlAuthenticationStrategyTests {
 		assertThat(this.sessionInformation.isExpired()).isFalse();
 	}
 
+	@Test
+	public void setMaximumSessionsWithNullValue() {
+		assertThatExceptionOfType(IllegalArgumentException.class)
+			.isThrownBy(() -> this.strategy.setMaximumSessions(null))
+			.withMessage("sessionLimit cannot be null");
+	}
+
+	@Test
+	public void noRegisteredSessionUsingSessionLimit() {
+		given(this.sessionRegistry.getAllSessions(any(), anyBoolean())).willReturn(Collections.emptyList());
+		this.strategy.setMaximumSessions(SessionLimit.of(1));
+		this.strategy.setExceptionIfMaximumExceeded(true);
+		this.strategy.onAuthentication(this.authentication, this.request, this.response);
+		// no exception
+	}
+
+	@Test
+	public void maxSessionsSameSessionIdUsingSessionLimit() {
+		MockHttpSession session = new MockHttpSession(new MockServletContext(), this.sessionInformation.getSessionId());
+		this.request.setSession(session);
+		given(this.sessionRegistry.getAllSessions(any(), anyBoolean()))
+			.willReturn(Collections.singletonList(this.sessionInformation));
+		this.strategy.setMaximumSessions(SessionLimit.of(1));
+		this.strategy.setExceptionIfMaximumExceeded(true);
+		this.strategy.onAuthentication(this.authentication, this.request, this.response);
+		// no exception
+	}
+
+	@Test
+	public void maxSessionsWithExceptionUsingSessionLimit() {
+		given(this.sessionRegistry.getAllSessions(any(), anyBoolean()))
+			.willReturn(Collections.singletonList(this.sessionInformation));
+		this.strategy.setMaximumSessions(SessionLimit.of(1));
+		this.strategy.setExceptionIfMaximumExceeded(true);
+		assertThatExceptionOfType(SessionAuthenticationException.class)
+			.isThrownBy(() -> this.strategy.onAuthentication(this.authentication, this.request, this.response));
+	}
+
+	@Test
+	public void maxSessionsExpireExistingUserUsingSessionLimit() {
+		given(this.sessionRegistry.getAllSessions(any(), anyBoolean()))
+			.willReturn(Collections.singletonList(this.sessionInformation));
+		this.strategy.setMaximumSessions(SessionLimit.of(1));
+		this.strategy.onAuthentication(this.authentication, this.request, this.response);
+		assertThat(this.sessionInformation.isExpired()).isTrue();
+	}
+
+	@Test
+	public void maxSessionsExpireLeastRecentExistingUserUsingSessionLimit() {
+		SessionInformation moreRecentSessionInfo = new SessionInformation(this.authentication.getPrincipal(), "unique",
+				new Date(1374766999999L));
+		given(this.sessionRegistry.getAllSessions(any(), anyBoolean()))
+			.willReturn(Arrays.asList(moreRecentSessionInfo, this.sessionInformation));
+		this.strategy.setMaximumSessions(SessionLimit.of(2));
+		this.strategy.onAuthentication(this.authentication, this.request, this.response);
+		assertThat(this.sessionInformation.isExpired()).isTrue();
+	}
+
+	@Test
+	public void onAuthenticationWhenMaxSessionsExceededByTwoThenTwoSessionsExpiredUsingSessionLimit() {
+		SessionInformation oldestSessionInfo = new SessionInformation(this.authentication.getPrincipal(), "unique1",
+				new Date(1374766134214L));
+		SessionInformation secondOldestSessionInfo = new SessionInformation(this.authentication.getPrincipal(),
+				"unique2", new Date(1374766134215L));
+		given(this.sessionRegistry.getAllSessions(any(), anyBoolean()))
+			.willReturn(Arrays.asList(oldestSessionInfo, secondOldestSessionInfo, this.sessionInformation));
+		this.strategy.setMaximumSessions(SessionLimit.of(2));
+		this.strategy.onAuthentication(this.authentication, this.request, this.response);
+		assertThat(oldestSessionInfo.isExpired()).isTrue();
+		assertThat(secondOldestSessionInfo.isExpired()).isTrue();
+		assertThat(this.sessionInformation.isExpired()).isFalse();
+	}
+
+	@Test
+	public void onAuthenticationWhenSessionLimitIsUnlimited() {
+		this.strategy.setMaximumSessions(SessionLimit.UNLIMITED);
+		this.strategy.onAuthentication(this.authentication, this.request, this.response);
+		verifyNoInteractions(this.sessionRegistry);
+	}
+
 	@Test
 	public void setMessageSourceNull() {
 		assertThatIllegalArgumentException().isThrownBy(() -> this.strategy.setMessageSource(null));

+ 59 - 0
web/src/test/java/org/springframework/security/web/session/SessionLimitTests.java

@@ -0,0 +1,59 @@
+/*
+ * Copyright 2002-2024 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.security.web.session;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.Mockito;
+
+import org.springframework.security.core.Authentication;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+
+/**
+ * @author Claudenir Freitas
+ * @since 6.5
+ */
+class SessionLimitTests {
+
+	private final Authentication authentication = Mockito.mock(Authentication.class);
+
+	@Test
+	void testUnlimitedInstance() {
+		SessionLimit sessionLimit = SessionLimit.UNLIMITED;
+		int result = sessionLimit.apply(this.authentication);
+		assertThat(result).isEqualTo(-1);
+	}
+
+	@ParameterizedTest
+	@ValueSource(ints = { -1, 1, 2, 3 })
+	void testInstanceWithValidMaxSessions(int maxSessions) {
+		SessionLimit sessionLimit = SessionLimit.of(maxSessions);
+		int result = sessionLimit.apply(this.authentication);
+		assertThat(result).isEqualTo(maxSessions);
+	}
+
+	@Test
+	void testInstanceWithInvalidMaxSessions() {
+		assertThatIllegalArgumentException().isThrownBy(() -> SessionLimit.of(0))
+			.withMessage(
+					"MaximumLogins must be either -1 to allow unlimited logins, or a positive integer to specify a maximum");
+	}
+
+}