Pārlūkot izejas kodu

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
Josh Cummings 8 mēneši atpakaļ
vecāks
revīzija
1104b45832

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

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

+ 11 - 0
config/src/main/kotlin/org/springframework/security/config/annotation/web/session/SessionConcurrencyDsl.kt

@@ -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)
             }

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

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

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

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

+ 65 - 4
config/src/test/kotlin/org/springframework/security/config/annotation/web/session/SessionConcurrencyDslTests.kt

@@ -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)
         }
     }
 }

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

@@ -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]
 ======

+ 0 - 1
web/src/main/java/org/springframework/security/web/authentication/session/ConcurrentSessionControlAuthenticationStrategy.java

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

+ 1 - 1
web/src/main/java/org/springframework/security/web/session/SessionLimit.java → web/src/main/java/org/springframework/security/web/authentication/session/SessionLimit.java

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

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

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

+ 1 - 1
web/src/test/java/org/springframework/security/web/session/SessionLimitTests.java → web/src/test/java/org/springframework/security/web/authentication/session/SessionLimitTests.java

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