Skip to content

Commit 1104b45

Browse files
committed
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
1 parent 1864577 commit 1104b45

File tree

10 files changed

+137
-12
lines changed

10 files changed

+137
-12
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
4848
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
4949
import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy;
50+
import org.springframework.security.web.authentication.session.SessionLimit;
5051
import org.springframework.security.web.context.DelegatingSecurityContextRepository;
5152
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
5253
import org.springframework.security.web.context.NullSecurityContextRepository;
@@ -59,7 +60,6 @@
5960
import org.springframework.security.web.session.ForceEagerSessionCreationFilter;
6061
import org.springframework.security.web.session.InvalidSessionStrategy;
6162
import org.springframework.security.web.session.SessionInformationExpiredStrategy;
62-
import org.springframework.security.web.session.SessionLimit;
6363
import org.springframework.security.web.session.SessionManagementFilter;
6464
import org.springframework.security.web.session.SimpleRedirectInvalidSessionStrategy;
6565
import org.springframework.security.web.session.SimpleRedirectSessionInformationExpiredStrategy;

config/src/main/kotlin/org/springframework/security/config/annotation/web/session/SessionConcurrencyDsl.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ package org.springframework.security.config.annotation.web.session
1919
import org.springframework.security.config.annotation.web.builders.HttpSecurity
2020
import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer
2121
import org.springframework.security.core.session.SessionRegistry
22+
import org.springframework.security.web.authentication.session.SessionLimit
2223
import org.springframework.security.web.session.SessionInformationExpiredStrategy
24+
import org.springframework.util.Assert
2325

2426
/**
2527
* A Kotlin DSL to configure the behaviour of multiple sessions using idiomatic
@@ -44,12 +46,21 @@ class SessionConcurrencyDsl {
4446
var expiredSessionStrategy: SessionInformationExpiredStrategy? = null
4547
var maxSessionsPreventsLogin: Boolean? = null
4648
var sessionRegistry: SessionRegistry? = null
49+
private var sessionLimit: SessionLimit? = null
50+
51+
fun maximumSessions(max: SessionLimit) {
52+
this.sessionLimit = max
53+
}
4754

4855
internal fun get(): (SessionManagementConfigurer<HttpSecurity>.ConcurrencyControlConfigurer) -> Unit {
56+
Assert.isTrue(maximumSessions == null || sessionLimit == null, "You cannot specify maximumSessions as both an Int and a SessionLimit. Please use only one.")
4957
return { sessionConcurrencyControl ->
5058
maximumSessions?.also {
5159
sessionConcurrencyControl.maximumSessions(maximumSessions!!)
5260
}
61+
sessionLimit?.also {
62+
sessionConcurrencyControl.maximumSessions(sessionLimit!!)
63+
}
5364
expiredUrl?.also {
5465
sessionConcurrencyControl.expiredUrl(expiredUrl)
5566
}

config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,12 @@
5959
import org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy;
6060
import org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy;
6161
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
62+
import org.springframework.security.web.authentication.session.SessionLimit;
6263
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
6364
import org.springframework.security.web.context.SecurityContextRepository;
6465
import org.springframework.security.web.savedrequest.RequestCache;
6566
import org.springframework.security.web.session.ConcurrentSessionFilter;
6667
import org.springframework.security.web.session.HttpSessionDestroyedEvent;
67-
import org.springframework.security.web.session.SessionLimit;
6868
import org.springframework.security.web.session.SessionManagementFilter;
6969
import org.springframework.test.web.servlet.MockMvc;
7070
import org.springframework.test.web.servlet.MvcResult;

config/src/test/java/org/springframework/security/config/http/HttpHeadersConfigTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
import org.springframework.security.config.test.SpringTestContext;
3636
import org.springframework.security.config.test.SpringTestContextExtension;
3737
import org.springframework.security.core.Authentication;
38-
import org.springframework.security.web.session.SessionLimit;
38+
import org.springframework.security.web.authentication.session.SessionLimit;
3939
import org.springframework.test.web.servlet.MockMvc;
4040
import org.springframework.test.web.servlet.ResultMatcher;
4141
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;

config/src/test/kotlin/org/springframework/security/config/annotation/web/session/SessionConcurrencyDslTests.kt

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,19 @@ package org.springframework.security.config.annotation.web.session
1818

1919
import io.mockk.every
2020
import io.mockk.mockkObject
21-
import java.util.Date
2221
import org.junit.jupiter.api.Test
2322
import org.junit.jupiter.api.extension.ExtendWith
2423
import org.springframework.beans.factory.annotation.Autowired
2524
import org.springframework.context.annotation.Bean
2625
import org.springframework.context.annotation.Configuration
2726
import org.springframework.mock.web.MockHttpSession
27+
import org.springframework.security.authorization.AuthorityAuthorizationManager
28+
import org.springframework.security.authorization.AuthorizationManager
2829
import org.springframework.security.config.annotation.web.builders.HttpSecurity
2930
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
31+
import org.springframework.security.config.annotation.web.invoke
3032
import org.springframework.security.config.test.SpringTestContext
3133
import org.springframework.security.config.test.SpringTestContextExtension
32-
import org.springframework.security.config.annotation.web.invoke
3334
import org.springframework.security.core.session.SessionInformation
3435
import org.springframework.security.core.session.SessionRegistry
3536
import org.springframework.security.core.session.SessionRegistryImpl
@@ -44,6 +45,7 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
4445
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
4546
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl
4647
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
48+
import java.util.*
4749

4850
/**
4951
* Tests for [SessionConcurrencyDsl]
@@ -173,16 +175,75 @@ class SessionConcurrencyDslTests {
173175
open fun sessionRegistry(): SessionRegistry = SESSION_REGISTRY
174176
}
175177

178+
@Test
179+
fun `session concurrency when session limit then no more sessions allowed`() {
180+
this.spring.register(MaximumSessionsFunctionConfig::class.java, UserDetailsConfig::class.java).autowire()
181+
182+
this.mockMvc.perform(post("/login")
183+
.with(csrf())
184+
.param("username", "user")
185+
.param("password", "password"))
186+
187+
this.mockMvc.perform(post("/login")
188+
.with(csrf())
189+
.param("username", "user")
190+
.param("password", "password"))
191+
.andExpect(status().isFound)
192+
.andExpect(redirectedUrl("/login?error"))
193+
194+
this.mockMvc.perform(post("/login")
195+
.with(csrf())
196+
.param("username", "admin")
197+
.param("password", "password"))
198+
.andExpect(status().isFound)
199+
.andExpect(redirectedUrl("/"))
200+
201+
this.mockMvc.perform(post("/login")
202+
.with(csrf())
203+
.param("username", "admin")
204+
.param("password", "password"))
205+
.andExpect(status().isFound)
206+
.andExpect(redirectedUrl("/"))
207+
}
208+
209+
@Configuration
210+
@EnableWebSecurity
211+
open class MaximumSessionsFunctionConfig {
212+
213+
@Bean
214+
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
215+
val isAdmin: AuthorizationManager<Any> = AuthorityAuthorizationManager.hasRole("ADMIN")
216+
http {
217+
sessionManagement {
218+
sessionConcurrency {
219+
maximumSessions {
220+
authentication -> if (isAdmin.authorize({ authentication }, null)!!.isGranted) -1 else 1
221+
}
222+
maxSessionsPreventsLogin = true
223+
}
224+
}
225+
formLogin { }
226+
}
227+
return http.build()
228+
}
229+
230+
}
231+
176232
@Configuration
177233
open class UserDetailsConfig {
178234
@Bean
179235
open fun userDetailsService(): UserDetailsService {
180-
val userDetails = User.withDefaultPasswordEncoder()
236+
val user = User.withDefaultPasswordEncoder()
181237
.username("user")
182238
.password("password")
183239
.roles("USER")
184240
.build()
185-
return InMemoryUserDetailsManager(userDetails)
241+
val admin = User.withDefaultPasswordEncoder()
242+
.username("admin")
243+
.password("password")
244+
.roles("ADMIN")
245+
.build()
246+
return InMemoryUserDetailsManager(user, admin)
186247
}
187248
}
188249
}

docs/modules/ROOT/pages/servlet/authentication/session-management.adoc

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,62 @@ XML::
399399

400400
This will prevent a user from logging in multiple times - a second login will cause the first to be invalidated.
401401

402-
Using Spring Boot, you can test the above configuration scenario the following way:
402+
You can also adjust this based on who the user is.
403+
For example, administrators may be able to have more than one session:
404+
405+
[tabs]
406+
======
407+
Java::
408+
+
409+
[source,java,role="primary"]
410+
----
411+
@Bean
412+
public SecurityFilterChain filterChain(HttpSecurity http) {
413+
AuthorizationManager<?> isAdmin = AuthorityAuthorizationManager.hasRole("ADMIN");
414+
http
415+
.sessionManagement(session -> session
416+
.maximumSessions((authentication) -> isAdmin.authorize(() -> authentication, null).isGranted() ? -1 : 1)
417+
);
418+
return http.build();
419+
}
420+
----
421+
422+
Kotlin::
423+
+
424+
[source,kotlin,role="secondary"]
425+
----
426+
@Bean
427+
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
428+
val isAdmin: AuthorizationManager<*> = AuthorityAuthorizationManager.hasRole("ADMIN")
429+
http {
430+
sessionManagement {
431+
sessionConcurrency {
432+
maximumSessions {
433+
authentication -> if (isAdmin.authorize({ authentication }, null)!!.isGranted) -1 else 1
434+
}
435+
}
436+
}
437+
}
438+
return http.build()
439+
}
440+
----
441+
442+
XML::
443+
+
444+
[source,xml,role="secondary"]
445+
----
446+
<http>
447+
...
448+
<session-management>
449+
<concurrency-control max-sessions-ref="sessionLimit" />
450+
</session-management>
451+
</http>
452+
453+
<b:bean id="sessionLimit" class="my.SessionLimitImplementation"/>
454+
----
455+
======
456+
457+
Using Spring Boot, you can test the above configurations in the following way:
403458

404459
[tabs]
405460
======

web/src/main/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategy.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
3434
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
3535
import org.springframework.security.web.session.ConcurrentSessionFilter;
36-
import org.springframework.security.web.session.SessionLimit;
3736
import org.springframework.security.web.session.SessionManagementFilter;
3837
import org.springframework.util.Assert;
3938

web/src/main/java/org/springframework/security/web/session/SessionLimit.java renamed to web/src/main/java/org/springframework/security/web/authentication/session/SessionLimit.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
package org.springframework.security.web.session;
17+
package org.springframework.security.web.authentication.session;
1818

1919
import java.util.function.Function;
2020

web/src/test/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategyTests.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
import org.springframework.security.core.Authentication;
3535
import org.springframework.security.core.session.SessionInformation;
3636
import org.springframework.security.core.session.SessionRegistry;
37-
import org.springframework.security.web.session.SessionLimit;
3837

3938
import static org.assertj.core.api.Assertions.assertThat;
4039
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;

web/src/test/java/org/springframework/security/web/session/SessionLimitTests.java renamed to web/src/test/java/org/springframework/security/web/authentication/session/SessionLimitTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
package org.springframework.security.web.session;
17+
package org.springframework.security.web.authentication.session;
1818

1919
import org.junit.jupiter.api.Test;
2020
import org.junit.jupiter.params.ParameterizedTest;

0 commit comments

Comments
 (0)