Skip to content

Commit 010496c

Browse files
Address SessionLimitStrategy
Closes gh-16206
1 parent 15b9a2a commit 010496c

File tree

12 files changed

+129
-51
lines changed

12 files changed

+129
-51
lines changed

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

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,6 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
126126

127127
private SessionRegistry sessionRegistry;
128128

129-
private Integer maximumSessions;
130-
131129
private String expiredUrl;
132130

133131
private boolean maxSessionsPreventsLogin;
@@ -332,7 +330,7 @@ public SessionManagementConfigurer<H> sessionFixation(
332330
* @return the {@link SessionManagementConfigurer} for further customizations
333331
*/
334332
public ConcurrencyControlConfigurer maximumSessions(int maximumSessions) {
335-
this.maximumSessions = maximumSessions;
333+
this.sessionLimitStrategy = SessionLimitStrategy.of(maximumSessions);
336334
this.propertiesThatRequireImplicitAuthentication.add("maximumSessions = " + maximumSessions);
337335
return new ConcurrencyControlConfigurer();
338336
}
@@ -573,9 +571,8 @@ private SessionAuthenticationStrategy getSessionAuthenticationStrategy(H http) {
573571
SessionRegistry sessionRegistry = getSessionRegistry(http);
574572
ConcurrentSessionControlAuthenticationStrategy concurrentSessionControlStrategy = new ConcurrentSessionControlAuthenticationStrategy(
575573
sessionRegistry);
576-
SessionLimitStrategy sessionLimitStrategyValue = getSessionLimitStrategy();
577-
if (sessionLimitStrategyValue != null) {
578-
concurrentSessionControlStrategy.setSessionLimitStrategy(sessionLimitStrategyValue);
574+
if (this.sessionLimitStrategy != null) {
575+
concurrentSessionControlStrategy.setMaximumSessions(this.sessionLimitStrategy);
579576
}
580577
concurrentSessionControlStrategy.setExceptionIfMaximumExceeded(this.maxSessionsPreventsLogin);
581578
concurrentSessionControlStrategy = postProcess(concurrentSessionControlStrategy);
@@ -594,17 +591,6 @@ private SessionAuthenticationStrategy getSessionAuthenticationStrategy(H http) {
594591
return this.sessionAuthenticationStrategy;
595592
}
596593

597-
private SessionLimitStrategy getSessionLimitStrategy() {
598-
if (this.sessionLimitStrategy != null) {
599-
return this.sessionLimitStrategy;
600-
}
601-
if (this.maximumSessions == null) {
602-
return null;
603-
}
604-
this.sessionLimitStrategy = SessionLimitStrategy.of(this.maximumSessions);
605-
return this.sessionLimitStrategy;
606-
}
607-
608594
private SessionRegistry getSessionRegistry(H http) {
609595
if (this.sessionRegistry == null) {
610596
this.sessionRegistry = getBeanOrNull(SessionRegistry.class);
@@ -631,7 +617,7 @@ private void registerDelegateApplicationListener(H http, ApplicationListener<?>
631617
* @return
632618
*/
633619
private boolean isConcurrentSessionControlEnabled() {
634-
return this.maximumSessions != null || this.sessionLimitStrategy != null;
620+
return this.sessionLimitStrategy != null;
635621
}
636622

637623
/**
@@ -723,7 +709,7 @@ private ConcurrencyControlConfigurer() {
723709
* @return the {@link ConcurrencyControlConfigurer} for further customizations
724710
*/
725711
public ConcurrencyControlConfigurer maximumSessions(int maximumSessions) {
726-
SessionManagementConfigurer.this.maximumSessions = maximumSessions;
712+
SessionManagementConfigurer.this.sessionLimitStrategy = SessionLimitStrategy.of(maximumSessions);
727713
return this;
728714
}
729715

config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ class HttpConfigurationBuilder {
122122

123123
private static final String ATT_SESSION_AUTH_STRATEGY_REF = "session-authentication-strategy-ref";
124124

125-
private static final String ATT_SESSION_LIMIT_STRATEGY_REF = "session-limit-strategy-ref";
125+
private static final String ATT_MAX_SESSIONS_REF = "max-sessions-ref";
126126

127127
private static final String ATT_MAX_SESSIONS = "max-sessions";
128128

@@ -495,7 +495,7 @@ else if (StringUtils.hasText(sessionAuthStratRef)) {
495495
}
496496
String sessionLimitStrategyRef = this.pc.getReaderContext()
497497
.getEnvironment()
498-
.resolvePlaceholders(sessionCtrlElt.getAttribute(ATT_SESSION_LIMIT_STRATEGY_REF));
498+
.resolvePlaceholders(sessionCtrlElt.getAttribute(ATT_MAX_SESSIONS_REF));
499499
if (StringUtils.hasText(sessionLimitStrategyRef)) {
500500
concurrentSessionStrategy.addPropertyReference("sessionLimitStrategy", sessionLimitStrategyRef);
501501
}
@@ -602,11 +602,10 @@ private void createConcurrencyControlFilterAndSessionRegistry(Element element) {
602602
source);
603603
}
604604
String maxSessions = element.getAttribute(ATT_MAX_SESSIONS);
605-
String sessionLimitStrategyRef = element.getAttribute(ATT_SESSION_LIMIT_STRATEGY_REF);
606-
if (StringUtils.hasText(maxSessions) && StringUtils.hasText(sessionLimitStrategyRef)) {
605+
String maxSessionsRef = element.getAttribute(ATT_MAX_SESSIONS_REF);
606+
if (StringUtils.hasText(maxSessions) && StringUtils.hasText(maxSessionsRef)) {
607607
this.pc.getReaderContext()
608-
.error("Cannot use 'max-sessions' attribute and 'session-limit-strategy-ref' attribute together.",
609-
source);
608+
.error("Cannot use 'max-sessions' attribute and 'max-sessions-ref' attribute together.", source);
610609
}
611610
if (StringUtils.hasText(expiryUrl)) {
612611
BeanDefinitionBuilder expiredSessionBldr = BeanDefinitionBuilder

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -936,7 +936,7 @@ concurrency-control.attlist &=
936936
attribute max-sessions {xsd:token}?
937937
concurrency-control.attlist &=
938938
## Allows injection of the SessionLimitStrategy instance used by the ConcurrentSessionControlAuthenticationStrategy
939-
attribute session-limit-strategy-ref {xsd:token}?
939+
attribute max-sessions-ref {xsd:token}?
940940
concurrency-control.attlist &=
941941
## 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.
942942
attribute expired-url {xsd:token}?

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2688,7 +2688,7 @@
26882688
</xs:documentation>
26892689
</xs:annotation>
26902690
</xs:attribute>
2691-
<xs:attribute name="session-limit-strategy-ref" type="xs:token">
2691+
<xs:attribute name="max-sessions-ref" type="xs:token">
26922692
<xs:annotation>
26932693
<xs:documentation>Allows injection of the SessionLimitStrategy instance used by the
26942694
ConcurrentSessionControlAuthenticationStrategy

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -711,10 +711,8 @@ SecurityFilterChain filterChain(HttpSecurity http, SessionLimitStrategy sessionL
711711
// @formatter:off
712712
http
713713
.formLogin(withDefaults())
714-
.sessionManagement((sessionManagement) ->
715-
sessionManagement
716-
.sessionConcurrency((sessionConcurrency) ->
717-
sessionConcurrency
714+
.sessionManagement((sessionManagement) -> sessionManagement
715+
.sessionConcurrency((sessionConcurrency) -> sessionConcurrency
718716
.sessionLimitStrategy(sessionLimitStrategy)
719717
.maxSessionsPreventsLogin(true)
720718
)

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

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -24,6 +24,7 @@
2424
import java.util.Set;
2525

2626
import com.google.common.collect.ImmutableMap;
27+
import jakarta.servlet.http.HttpSession;
2728
import org.junit.jupiter.api.Test;
2829
import org.junit.jupiter.api.extension.ExtendWith;
2930

@@ -35,12 +36,17 @@
3536
import org.springframework.security.config.test.SpringTestContextExtension;
3637
import org.springframework.test.web.servlet.MockMvc;
3738
import org.springframework.test.web.servlet.ResultMatcher;
39+
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
3840
import org.springframework.web.bind.annotation.GetMapping;
3941
import org.springframework.web.bind.annotation.RestController;
4042

43+
import static org.assertj.core.api.Assertions.assertThat;
4144
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
45+
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
4246
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
47+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
4348
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
49+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
4450
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
4551

4652
/**
@@ -49,6 +55,7 @@
4955
* @author Josh Cummings
5056
* @author Rafiullah Hamedy
5157
* @author Marcus Da Coregio
58+
* @author Claudenir Freitas
5259
*/
5360
@ExtendWith(SpringTestContextExtension.class)
5461
public class HttpHeadersConfigTests {
@@ -782,6 +789,57 @@ public void requestWhenCrossOriginPoliciesRespondsCrossOriginPolicies() throws E
782789
// @formatter:on
783790
}
784791

792+
@Test
793+
public void requestWhenSessionManagementConcurrencyControlMaxSessionIsOne() throws Exception {
794+
System.setProperty("security.session-management.concurrency-control.max-sessions", "1");
795+
this.spring.configLocations(this.xml("DefaultsSessionManagementConcurrencyControlMaxSessions")).autowire();
796+
// @formatter:off
797+
MockHttpServletRequestBuilder requestBuilder = post("/login")
798+
.with(csrf())
799+
.param("username", "user")
800+
.param("password", "password");
801+
HttpSession firstSession = this.mvc.perform(requestBuilder)
802+
.andExpect(status().is3xxRedirection())
803+
.andExpect(redirectedUrl("/"))
804+
.andReturn()
805+
.getRequest()
806+
.getSession(false);
807+
// @formatter:on
808+
assertThat(firstSession).isNotNull();
809+
// @formatter:off
810+
this.mvc.perform(requestBuilder)
811+
.andExpect(status().isFound())
812+
.andExpect(redirectedUrl("/login?error"));
813+
// @formatter:on
814+
}
815+
816+
@Test
817+
public void requestWhenSessionManagementConcurrencyControlMaxSessionIsUnlimited() throws Exception {
818+
System.setProperty("security.session-management.concurrency-control.max-sessions", "-1");
819+
this.spring.configLocations(this.xml("DefaultsSessionManagementConcurrencyControlMaxSessions")).autowire();
820+
// @formatter:off
821+
MockHttpServletRequestBuilder requestBuilder = post("/login")
822+
.with(csrf())
823+
.param("username", "user")
824+
.param("password", "password");
825+
HttpSession firstSession = this.mvc.perform(requestBuilder)
826+
.andExpect(status().is3xxRedirection())
827+
.andExpect(redirectedUrl("/"))
828+
.andReturn()
829+
.getRequest()
830+
.getSession(false);
831+
assertThat(firstSession).isNotNull();
832+
HttpSession secondSession = this.mvc.perform(requestBuilder)
833+
.andExpect(status().is3xxRedirection())
834+
.andExpect(redirectedUrl("/"))
835+
.andReturn()
836+
.getRequest()
837+
.getSession(false);
838+
assertThat(secondSession).isNotNull();
839+
// @formatter:on
840+
assertThat(firstSession.getId()).isNotEqualTo(secondSession.getId());
841+
}
842+
785843
private static ResultMatcher includesDefaults() {
786844
return includes(defaultHeaders);
787845
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
~ Copyright 2002-2024 the original author or authors.
4+
~
5+
~ Licensed under the Apache License, Version 2.0 (the "License");
6+
~ you may not use this file except in compliance with the License.
7+
~ You may obtain a copy of the License at
8+
~
9+
~ https://www.apache.org/licenses/LICENSE-2.0
10+
~
11+
~ Unless required by applicable law or agreed to in writing, software
12+
~ distributed under the License is distributed on an "AS IS" BASIS,
13+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
~ See the License for the specific language governing permissions and
15+
~ limitations under the License.
16+
-->
17+
18+
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
19+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
20+
xmlns="http://www.springframework.org/schema/security"
21+
xsi:schemaLocation="
22+
http://www.springframework.org/schema/security
23+
https://www.springframework.org/schema/security/spring-security.xsd
24+
http://www.springframework.org/schema/beans
25+
https://www.springframework.org/schema/beans/spring-beans.xsd">
26+
27+
<http auto-config="true">
28+
<session-management>
29+
<concurrency-control max-sessions="${security.session-management.concurrency-control.max-sessions}"
30+
error-if-maximum-exceeded="true"/>
31+
</session-management>
32+
<intercept-url pattern="/**" access="permitAll"/>
33+
</http>
34+
35+
<b:bean name="simple" class="org.springframework.security.config.http.HttpHeadersConfigTests.SimpleController"/>
36+
37+
<b:import resource="userservice.xml"/>
38+
</b:beans>

docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2168,8 +2168,8 @@ Allows injection of the ExpiredSessionStrategy instance used by the ConcurrentSe
21682168
Maps to the `maximumSessions` property of `ConcurrentSessionControlAuthenticationStrategy`.
21692169
Specify `-1` as the value to support unlimited sessions.
21702170

2171-
[[nsa-concurrency-control-session-limit-strategy-ref]]
2172-
* **session-limit**
2171+
[[nsa-concurrency-control-max-sessions-ref]]
2172+
* **max-sessions-ref**
21732173
Allows injection of the SessionLimitStrategy instance used by the ConcurrentSessionControlAuthenticationStrategy
21742174

21752175
[[nsa-concurrency-control-session-registry-alias]]

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ public void onAuthentication(Authentication authentication, HttpServletRequest r
131131
* @return either -1 meaning unlimited, or a positive integer to limit (never zero)
132132
*/
133133
protected int getMaximumSessionsForThisUser(Authentication authentication) {
134-
return this.sessionLimitStrategy.resolve(authentication);
134+
return this.sessionLimitStrategy.apply(authentication);
135135
}
136136

137137
/**
@@ -187,7 +187,7 @@ public void setMaximumSessions(int maximumSessions) {
187187
* unlimited sessions.
188188
* @param sessionLimitStrategy the session limit strategy
189189
*/
190-
public void setSessionLimitStrategy(SessionLimitStrategy sessionLimitStrategy) {
190+
public void setMaximumSessions(SessionLimitStrategy sessionLimitStrategy) {
191191
Assert.notNull(sessionLimitStrategy, "sessionLimitStrategy cannot be null");
192192
this.sessionLimitStrategy = sessionLimitStrategy;
193193
}

web/src/main/java/org/springframework/security/web/session/SessionLimitStrategy.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.security.web.session;
1818

19+
import java.util.function.Function;
20+
1921
import org.springframework.security.core.Authentication;
2022
import org.springframework.util.Assert;
2123

@@ -26,10 +28,7 @@
2628
* @author Claudenir Freitas
2729
* @since 6.5
2830
*/
29-
@FunctionalInterface
30-
public interface SessionLimitStrategy {
31-
32-
int resolve(Authentication authentication);
31+
public interface SessionLimitStrategy extends Function<Authentication, Integer> {
3332

3433
/**
3534
* Represents unlimited sessions.

0 commit comments

Comments
 (0)