Polish SessionLimit

- Move to the web.authentication.session package since it is only needed
by web.authentication.session elements and does not access any other web
element itself.
- Add Kotlin support
- Add documentation

Issue gh-16206
This commit is contained in:
Josh Cummings 2024-12-18 16:43:19 -07:00
parent 1864577e98
commit 1104b45832
No known key found for this signature in database
GPG Key ID: A306A51F43B8E5A5
10 changed files with 137 additions and 12 deletions

View File

@ -47,6 +47,7 @@ import org.springframework.security.web.authentication.session.NullAuthenticated
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy;
import org.springframework.security.web.authentication.session.SessionLimit;
import org.springframework.security.web.context.DelegatingSecurityContextRepository;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.NullSecurityContextRepository;
@ -59,7 +60,6 @@ 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;

View File

@ -19,7 +19,9 @@ package org.springframework.security.config.annotation.web.session
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer
import org.springframework.security.core.session.SessionRegistry
import org.springframework.security.web.authentication.session.SessionLimit
import org.springframework.security.web.session.SessionInformationExpiredStrategy
import org.springframework.util.Assert
/**
* A Kotlin DSL to configure the behaviour of multiple sessions using idiomatic
@ -44,12 +46,21 @@ class SessionConcurrencyDsl {
var expiredSessionStrategy: SessionInformationExpiredStrategy? = null
var maxSessionsPreventsLogin: Boolean? = null
var sessionRegistry: SessionRegistry? = null
private var sessionLimit: SessionLimit? = null
fun maximumSessions(max: SessionLimit) {
this.sessionLimit = max
}
internal fun get(): (SessionManagementConfigurer<HttpSecurity>.ConcurrencyControlConfigurer) -> Unit {
Assert.isTrue(maximumSessions == null || sessionLimit == null, "You cannot specify maximumSessions as both an Int and a SessionLimit. Please use only one.")
return { sessionConcurrencyControl ->
maximumSessions?.also {
sessionConcurrencyControl.maximumSessions(maximumSessions!!)
}
sessionLimit?.also {
sessionConcurrencyControl.maximumSessions(sessionLimit!!)
}
expiredUrl?.also {
sessionConcurrencyControl.expiredUrl(expiredUrl)
}

View File

@ -59,12 +59,12 @@ import org.springframework.security.web.authentication.session.ChangeSessionIdAu
import org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionLimit;
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
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;

View File

@ -35,7 +35,7 @@ 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.security.web.authentication.session.SessionLimit;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultMatcher;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;

View File

@ -18,18 +18,19 @@ package org.springframework.security.config.annotation.web.session
import io.mockk.every
import io.mockk.mockkObject
import java.util.Date
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.mock.web.MockHttpSession
import org.springframework.security.authorization.AuthorityAuthorizationManager
import org.springframework.security.authorization.AuthorizationManager
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.invoke
import org.springframework.security.config.test.SpringTestContext
import org.springframework.security.config.test.SpringTestContextExtension
import org.springframework.security.config.annotation.web.invoke
import org.springframework.security.core.session.SessionInformation
import org.springframework.security.core.session.SessionRegistry
import org.springframework.security.core.session.SessionRegistryImpl
@ -44,6 +45,7 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import java.util.*
/**
* Tests for [SessionConcurrencyDsl]
@ -173,16 +175,75 @@ class SessionConcurrencyDslTests {
open fun sessionRegistry(): SessionRegistry = SESSION_REGISTRY
}
@Test
fun `session concurrency when session limit then no more sessions allowed`() {
this.spring.register(MaximumSessionsFunctionConfig::class.java, UserDetailsConfig::class.java).autowire()
this.mockMvc.perform(post("/login")
.with(csrf())
.param("username", "user")
.param("password", "password"))
this.mockMvc.perform(post("/login")
.with(csrf())
.param("username", "user")
.param("password", "password"))
.andExpect(status().isFound)
.andExpect(redirectedUrl("/login?error"))
this.mockMvc.perform(post("/login")
.with(csrf())
.param("username", "admin")
.param("password", "password"))
.andExpect(status().isFound)
.andExpect(redirectedUrl("/"))
this.mockMvc.perform(post("/login")
.with(csrf())
.param("username", "admin")
.param("password", "password"))
.andExpect(status().isFound)
.andExpect(redirectedUrl("/"))
}
@Configuration
@EnableWebSecurity
open class MaximumSessionsFunctionConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
val isAdmin: AuthorizationManager<Any> = AuthorityAuthorizationManager.hasRole("ADMIN")
http {
sessionManagement {
sessionConcurrency {
maximumSessions {
authentication -> if (isAdmin.authorize({ authentication }, null)!!.isGranted) -1 else 1
}
maxSessionsPreventsLogin = true
}
}
formLogin { }
}
return http.build()
}
}
@Configuration
open class UserDetailsConfig {
@Bean
open fun userDetailsService(): UserDetailsService {
val userDetails = User.withDefaultPasswordEncoder()
val user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build()
return InMemoryUserDetailsManager(userDetails)
val admin = User.withDefaultPasswordEncoder()
.username("admin")
.password("password")
.roles("ADMIN")
.build()
return InMemoryUserDetailsManager(user, admin)
}
}
}

View File

@ -399,7 +399,62 @@ XML::
This will prevent a user from logging in multiple times - a second login will cause the first to be invalidated.
Using Spring Boot, you can test the above configuration scenario the following way:
You can also adjust this based on who the user is.
For example, administrators may be able to have more than one session:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
AuthorizationManager<?> isAdmin = AuthorityAuthorizationManager.hasRole("ADMIN");
http
.sessionManagement(session -> session
.maximumSessions((authentication) -> isAdmin.authorize(() -> authentication, null).isGranted() ? -1 : 1)
);
return http.build();
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
val isAdmin: AuthorizationManager<*> = AuthorityAuthorizationManager.hasRole("ADMIN")
http {
sessionManagement {
sessionConcurrency {
maximumSessions {
authentication -> if (isAdmin.authorize({ authentication }, null)!!.isGranted) -1 else 1
}
}
}
}
return http.build()
}
----
XML::
+
[source,xml,role="secondary"]
----
<http>
...
<session-management>
<concurrency-control max-sessions-ref="sessionLimit" />
</session-management>
</http>
<b:bean id="sessionLimit" class="my.SessionLimitImplementation"/>
----
======
Using Spring Boot, you can test the above configurations in the following way:
[tabs]
======

View File

@ -33,7 +33,6 @@ 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;

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.security.web.session;
package org.springframework.security.web.authentication.session;
import java.util.function.Function;

View File

@ -34,7 +34,6 @@ 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;

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.security.web.session;
package org.springframework.security.web.authentication.session;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;