Browse Source

Configuration of session management strategies

This commit adds an ExpiredSessionStrategy for the ConcurrentSessionFilter
analogous to the InvalidSessionStrategy for the SessionManagementFilter. It also
adds a configuration option for both the InvalidSessionStrategy and
ExpiredSessionStrategy to the XML namespace and Java configuration.

Fixes gh-3794
Fixes gh-3795
Marten Deinum 9 years ago
parent
commit
b88418b94a

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

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2013 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -47,8 +47,10 @@ import org.springframework.security.web.context.SecurityContextRepository;
 import org.springframework.security.web.savedrequest.NullRequestCache;
 import org.springframework.security.web.savedrequest.RequestCache;
 import org.springframework.security.web.session.ConcurrentSessionFilter;
+import org.springframework.security.web.session.ExpiredSessionStrategy;
 import org.springframework.security.web.session.InvalidSessionStrategy;
 import org.springframework.security.web.session.SessionManagementFilter;
+import org.springframework.security.web.session.SimpleRedirectExpiredSessionStrategy;
 import org.springframework.security.web.session.SimpleRedirectInvalidSessionStrategy;
 import org.springframework.util.Assert;
 
@@ -89,13 +91,14 @@ import org.springframework.util.Assert;
  * @see SessionManagementFilter
  * @see ConcurrentSessionFilter
  */
-public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>> extends
-		AbstractHttpConfigurer<SessionManagementConfigurer<H>, H> {
+public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
+		extends AbstractHttpConfigurer<SessionManagementConfigurer<H>, H> {
 	private final SessionAuthenticationStrategy DEFAULT_SESSION_FIXATION_STRATEGY = createDefaultSessionFixationProtectionStrategy();
-	private SessionAuthenticationStrategy sessionFixationAuthenticationStrategy = DEFAULT_SESSION_FIXATION_STRATEGY;
+	private SessionAuthenticationStrategy sessionFixationAuthenticationStrategy = this.DEFAULT_SESSION_FIXATION_STRATEGY;
 	private SessionAuthenticationStrategy sessionAuthenticationStrategy;
 	private SessionAuthenticationStrategy providedSessionAuthenticationStrategy;
 	private InvalidSessionStrategy invalidSessionStrategy;
+	private ExpiredSessionStrategy expiredSessionStrategy;
 	private List<SessionAuthenticationStrategy> sessionAuthenticationStrategies = new ArrayList<SessionAuthenticationStrategy>();
 	private SessionRegistry sessionRegistry;
 	private Integer maximumSessions;
@@ -131,10 +134,12 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
 	 * Setting this attribute will inject the provided invalidSessionStrategy into the
 	 * {@link SessionManagementFilter}. When an invalid session ID is submitted, the
 	 * strategy will be invoked, redirecting to the configured URL.
-	 * @param invalidSessionStrategy the strategy to use when an invalid session ID is submitted.
+	 * @param invalidSessionStrategy the strategy to use when an invalid session ID is
+	 * submitted.
 	 * @return the {@link SessionManagementConfigurer} for further customization
 	 */
-	public SessionManagementConfigurer<H> invalidSessionStrategy(InvalidSessionStrategy invalidSessionStrategy) {
+	public SessionManagementConfigurer<H> invalidSessionStrategy(
+			InvalidSessionStrategy invalidSessionStrategy) {
 		Assert.notNull(invalidSessionStrategy, "invalidSessionStrategy");
 		this.invalidSessionStrategy = invalidSessionStrategy;
 		return this;
@@ -198,7 +203,8 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
 	 * {@link SessionAuthenticationStrategy} the supplied sessionAuthenticationStrategy,
 	 * {@link RegisterSessionAuthenticationStrategy}.
 	 *
-	 * NOTE: Supplying a custom {@link SessionAuthenticationStrategy} will override the default provided {@link SessionFixationProtectionStrategy}.
+	 * NOTE: Supplying a custom {@link SessionAuthenticationStrategy} will override the
+	 * default provided {@link SessionFixationProtectionStrategy}.
 	 *
 	 * @param sessionAuthenticationStrategy
 	 * @return the {@link SessionManagementConfigurer} for further customizations
@@ -244,7 +250,8 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
 	 */
 	private void setSessionFixationAuthenticationStrategy(
 			SessionAuthenticationStrategy sessionFixationAuthenticationStrategy) {
-		this.sessionFixationAuthenticationStrategy = postProcess(sessionFixationAuthenticationStrategy);
+		this.sessionFixationAuthenticationStrategy = postProcess(
+				sessionFixationAuthenticationStrategy);
 	}
 
 	/**
@@ -273,7 +280,8 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
 		 * @return the {@link SessionManagementConfigurer} for further customizations
 		 */
 		public SessionManagementConfigurer<H> migrateSession() {
-			setSessionFixationAuthenticationStrategy(new SessionFixationProtectionStrategy());
+			setSessionFixationAuthenticationStrategy(
+					new SessionFixationProtectionStrategy());
 			return SessionManagementConfigurer.this;
 		}
 
@@ -288,7 +296,8 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
 		 * @throws IllegalStateException if the container is not Servlet 3.1 or newer.
 		 */
 		public SessionManagementConfigurer<H> changeSessionId() {
-			setSessionFixationAuthenticationStrategy(new ChangeSessionIdAuthenticationStrategy());
+			setSessionFixationAuthenticationStrategy(
+					new ChangeSessionIdAuthenticationStrategy());
 			return SessionManagementConfigurer.this;
 		}
 
@@ -301,7 +310,8 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
 		 * @return the {@link SessionManagementConfigurer} for further customizations
 		 */
 		public SessionManagementConfigurer<H> none() {
-			setSessionFixationAuthenticationStrategy(new NullAuthenticatedSessionStrategy());
+			setSessionFixationAuthenticationStrategy(
+					new NullAuthenticatedSessionStrategy());
 			return SessionManagementConfigurer.this;
 		}
 	}
@@ -326,6 +336,12 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
 			return this;
 		}
 
+		public ConcurrencyControlConfigurer expiredSessionStrategy(
+				ExpiredSessionStrategy expiredSessionStrategy) {
+			SessionManagementConfigurer.this.expiredSessionStrategy = expiredSessionStrategy;
+			return this;
+		}
+
 		/**
 		 * If true, prevents a user from authenticating when the
 		 * {@link #maximumSessions(int)} has been reached. Otherwise (default), the user
@@ -384,7 +400,8 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
 			}
 			else {
 				HttpSessionSecurityContextRepository httpSecurityRepository = new HttpSessionSecurityContextRepository();
-				httpSecurityRepository.setDisableUrlRewriting(!enableSessionUrlRewriting);
+				httpSecurityRepository
+						.setDisableUrlRewriting(!this.enableSessionUrlRewriting);
 				httpSecurityRepository.setAllowSessionCreation(isAllowSessionCreation());
 				AuthenticationTrustResolver trustResolver = http
 						.getSharedObject(AuthenticationTrustResolver.class);
@@ -413,15 +430,14 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
 				.getSharedObject(SecurityContextRepository.class);
 		SessionManagementFilter sessionManagementFilter = new SessionManagementFilter(
 				securityContextRepository, getSessionAuthenticationStrategy(http));
-		if (sessionAuthenticationErrorUrl != null) {
-			sessionManagementFilter
-					.setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler(
-							sessionAuthenticationErrorUrl));
+		if (this.sessionAuthenticationErrorUrl != null) {
+			sessionManagementFilter.setAuthenticationFailureHandler(
+					new SimpleUrlAuthenticationFailureHandler(
+							this.sessionAuthenticationErrorUrl));
 		}
 		InvalidSessionStrategy strategy = getInvalidSessionStrategy();
 		if (strategy != null) {
-			sessionManagementFilter
-					.setInvalidSessionStrategy(strategy);
+			sessionManagementFilter.setInvalidSessionStrategy(strategy);
 		}
 		AuthenticationTrustResolver trustResolver = http
 				.getSharedObject(AuthenticationTrustResolver.class);
@@ -433,7 +449,10 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
 		http.addFilter(sessionManagementFilter);
 		if (isConcurrentSessionControlEnabled()) {
 			ConcurrentSessionFilter concurrentSessionFilter = new ConcurrentSessionFilter(
-					getSessionRegistry(http), expiredUrl);
+					getSessionRegistry(http));
+			concurrentSessionFilter
+					.setExpiredSessionStrategy(getExpiredSessionStrategy());
+
 			concurrentSessionFilter = postProcess(concurrentSessionFilter);
 			http.addFilter(concurrentSessionFilter);
 		}
@@ -447,14 +466,30 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
 	 * @return the {@link InvalidSessionStrategy} to use
 	 */
 	InvalidSessionStrategy getInvalidSessionStrategy() {
-		if(invalidSessionStrategy != null) {
-			return invalidSessionStrategy;
+		if (this.invalidSessionStrategy != null) {
+			return this.invalidSessionStrategy;
 		}
-		if (invalidSessionUrl != null) {
-			invalidSessionStrategy = new SimpleRedirectInvalidSessionStrategy(
-					invalidSessionUrl);
+		if (this.invalidSessionUrl != null) {
+			this.invalidSessionStrategy = new SimpleRedirectInvalidSessionStrategy(
+					this.invalidSessionUrl);
 		}
-		return invalidSessionStrategy;
+		return this.invalidSessionStrategy;
+	}
+
+	ExpiredSessionStrategy getExpiredSessionStrategy() {
+		if (this.expiredSessionStrategy != null) {
+			return this.expiredSessionStrategy;
+		}
+
+		if (this.expiredUrl == null) {
+			return null;
+		}
+
+		if (this.expiredSessionStrategy == null) {
+			this.expiredSessionStrategy = new SimpleRedirectExpiredSessionStrategy(
+					this.expiredUrl);
+		}
+		return this.expiredSessionStrategy;
 	}
 
 	/**
@@ -462,7 +497,7 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
 	 * @return the {@link SessionCreationPolicy}
 	 */
 	SessionCreationPolicy getSessionCreationPolicy() {
-		return sessionPolicy;
+		return this.sessionPolicy;
 	}
 
 	/**
@@ -471,8 +506,8 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
 	 * @return true if the {@link SessionCreationPolicy} allows session creation
 	 */
 	private boolean isAllowSessionCreation() {
-		return SessionCreationPolicy.ALWAYS == sessionPolicy
-				|| SessionCreationPolicy.IF_REQUIRED == sessionPolicy;
+		return SessionCreationPolicy.ALWAYS == this.sessionPolicy
+				|| SessionCreationPolicy.IF_REQUIRED == this.sessionPolicy;
 	}
 
 	/**
@@ -480,7 +515,7 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
 	 * @return
 	 */
 	private boolean isStateless() {
-		return SessionCreationPolicy.STATELESS == sessionPolicy;
+		return SessionCreationPolicy.STATELESS == this.sessionPolicy;
 	}
 
 	/**
@@ -491,50 +526,52 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
 	 * @return the {@link SessionAuthenticationStrategy} to use
 	 */
 	private SessionAuthenticationStrategy getSessionAuthenticationStrategy(H http) {
-		if (sessionAuthenticationStrategy != null) {
-			return sessionAuthenticationStrategy;
+		if (this.sessionAuthenticationStrategy != null) {
+			return this.sessionAuthenticationStrategy;
 		}
-		List<SessionAuthenticationStrategy> delegateStrategies = sessionAuthenticationStrategies;
+		List<SessionAuthenticationStrategy> delegateStrategies = this.sessionAuthenticationStrategies;
 		SessionAuthenticationStrategy defaultSessionAuthenticationStrategy;
-		if (providedSessionAuthenticationStrategy == null) {
+		if (this.providedSessionAuthenticationStrategy == null) {
 			// If a user provided SessionAuthenticationStrategy is not supplied
 			// then default to SessionFixationProtectionStrategy
-			defaultSessionAuthenticationStrategy = postProcess(sessionFixationAuthenticationStrategy);
-		} else {
-			defaultSessionAuthenticationStrategy = providedSessionAuthenticationStrategy;
+			defaultSessionAuthenticationStrategy = postProcess(
+					this.sessionFixationAuthenticationStrategy);
+		}
+		else {
+			defaultSessionAuthenticationStrategy = this.providedSessionAuthenticationStrategy;
 		}
 		if (isConcurrentSessionControlEnabled()) {
 			SessionRegistry sessionRegistry = getSessionRegistry(http);
 			ConcurrentSessionControlAuthenticationStrategy concurrentSessionControlStrategy = new ConcurrentSessionControlAuthenticationStrategy(
 					sessionRegistry);
-			concurrentSessionControlStrategy.setMaximumSessions(maximumSessions);
+			concurrentSessionControlStrategy.setMaximumSessions(this.maximumSessions);
 			concurrentSessionControlStrategy
-					.setExceptionIfMaximumExceeded(maxSessionsPreventsLogin);
-			concurrentSessionControlStrategy = postProcess(concurrentSessionControlStrategy);
+					.setExceptionIfMaximumExceeded(this.maxSessionsPreventsLogin);
+			concurrentSessionControlStrategy = postProcess(
+					concurrentSessionControlStrategy);
 
 			RegisterSessionAuthenticationStrategy registerSessionStrategy = new RegisterSessionAuthenticationStrategy(
 					sessionRegistry);
 			registerSessionStrategy = postProcess(registerSessionStrategy);
 
-			delegateStrategies.addAll(Arrays.asList(
-					concurrentSessionControlStrategy,
-					defaultSessionAuthenticationStrategy,
-					registerSessionStrategy));
-		} else {
+			delegateStrategies.addAll(Arrays.asList(concurrentSessionControlStrategy,
+					defaultSessionAuthenticationStrategy, registerSessionStrategy));
+		}
+		else {
 			delegateStrategies.add(defaultSessionAuthenticationStrategy);
 		}
-		sessionAuthenticationStrategy = postProcess(new CompositeSessionAuthenticationStrategy(
-				delegateStrategies));
-		return sessionAuthenticationStrategy;
+		this.sessionAuthenticationStrategy = postProcess(
+				new CompositeSessionAuthenticationStrategy(delegateStrategies));
+		return this.sessionAuthenticationStrategy;
 	}
 
 	private SessionRegistry getSessionRegistry(H http) {
-		if (sessionRegistry == null) {
+		if (this.sessionRegistry == null) {
 			SessionRegistryImpl sessionRegistry = new SessionRegistryImpl();
 			registerDelegateApplicationListener(http, sessionRegistry);
 			this.sessionRegistry = sessionRegistry;
 		}
-		return sessionRegistry;
+		return this.sessionRegistry;
 	}
 
 	private void registerDelegateApplicationListener(H http,
@@ -558,7 +595,7 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
 	 * @return
 	 */
 	private boolean isConcurrentSessionControlEnabled() {
-		return maximumSessions != null;
+		return this.maximumSessions != null;
 	}
 
 	/**

+ 30 - 7
config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java

@@ -15,13 +15,9 @@
  */
 package org.springframework.security.config.http;
 
-import static org.springframework.security.config.http.HttpSecurityBeanDefinitionParser.*;
-import static org.springframework.security.config.http.SecurityFilters.*;
-
 import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.List;
-
 import javax.servlet.ServletRequest;
 import javax.servlet.http.HttpServletRequest;
 
@@ -69,6 +65,7 @@ import org.springframework.security.web.savedrequest.RequestCacheAwareFilter;
 import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter;
 import org.springframework.security.web.session.ConcurrentSessionFilter;
 import org.springframework.security.web.session.SessionManagementFilter;
+import org.springframework.security.web.session.SimpleRedirectExpiredSessionStrategy;
 import org.springframework.security.web.session.SimpleRedirectInvalidSessionStrategy;
 import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 import org.springframework.util.ClassUtils;
@@ -77,6 +74,9 @@ import org.springframework.util.StringUtils;
 import org.springframework.util.xml.DomUtils;
 import org.w3c.dom.Element;
 
+import static org.springframework.security.config.http.HttpSecurityBeanDefinitionParser.*;
+import static org.springframework.security.config.http.SecurityFilters.*;
+
 /**
  * Stateful class which helps HttpSecurityBDP to create the configuration for the
  * &lt;http&gt; element.
@@ -97,7 +97,7 @@ class HttpConfigurationBuilder {
 	private static final String ATT_SESSION_AUTH_STRATEGY_REF = "session-authentication-strategy-ref";
 	private static final String ATT_SESSION_AUTH_ERROR_URL = "session-authentication-error-url";
 	private static final String ATT_SECURITY_CONTEXT_REPOSITORY = "security-context-repository-ref";
-
+	private static final String ATT_INVALID_SESSION_STRATEGY_REF = "invalid-session-strategy-ref";
 	private static final String ATT_DISABLE_URL_REWRITING = "disable-url-rewriting";
 
 	private static final String ATT_ACCESS_MGR = "access-decision-manager-ref";
@@ -289,6 +289,7 @@ class HttpConfigurationBuilder {
 
 		String sessionFixationAttribute = null;
 		String invalidSessionUrl = null;
+		String invalidSessionStrategyRef = null;
 		String sessionAuthStratRef = null;
 		String errorUrl = null;
 
@@ -304,6 +305,8 @@ class HttpConfigurationBuilder {
 			sessionFixationAttribute = sessionMgmtElt
 					.getAttribute(ATT_SESSION_FIXATION_PROTECTION);
 			invalidSessionUrl = sessionMgmtElt.getAttribute(ATT_INVALID_SESSION_URL);
+			invalidSessionStrategyRef = sessionMgmtElt.getAttribute(ATT_INVALID_SESSION_STRATEGY_REF);
+
 			sessionAuthStratRef = sessionMgmtElt
 					.getAttribute(ATT_SESSION_AUTH_STRATEGY_REF);
 			errorUrl = sessionMgmtElt.getAttribute(ATT_SESSION_AUTH_ERROR_URL);
@@ -311,6 +314,11 @@ class HttpConfigurationBuilder {
 					Elements.CONCURRENT_SESSIONS);
 			sessionControlEnabled = sessionCtrlElt != null;
 
+			if (StringUtils.hasText(invalidSessionUrl) && StringUtils.hasText(invalidSessionStrategyRef)) {
+				pc.getReaderContext().error(ATT_INVALID_SESSION_URL + " attribute cannot be used in combination with" +
+						" the " + ATT_INVALID_SESSION_STRATEGY_REF + " attribute.", sessionMgmtElt);
+			}
+
 			if (sessionControlEnabled) {
 				if (StringUtils.hasText(sessionAuthStratRef)) {
 					pc.getReaderContext().error(
@@ -438,12 +446,16 @@ class HttpConfigurationBuilder {
 
 		}
 
+
+
 		if (StringUtils.hasText(invalidSessionUrl)) {
 			BeanDefinitionBuilder invalidSessionBldr = BeanDefinitionBuilder
 					.rootBeanDefinition(SimpleRedirectInvalidSessionStrategy.class);
 			invalidSessionBldr.addConstructorArgValue(invalidSessionUrl);
 			invalidSession = invalidSessionBldr.getBeanDefinition();
 			sessionMgmtFilter.addPropertyValue("invalidSessionStrategy", invalidSession);
+		} else if (StringUtils.hasText(invalidSessionStrategyRef)) {
+			sessionMgmtFilter.addPropertyReference("invalidSessionStrategy", invalidSessionStrategyRef);
 		}
 
 		sessionMgmtFilter.addConstructorArgReference(sessionAuthStratRef);
@@ -454,6 +466,7 @@ class HttpConfigurationBuilder {
 
 	private void createConcurrencyControlFilterAndSessionRegistry(Element element) {
 		final String ATT_EXPIRY_URL = "expired-url";
+		final String ATT_EXPIRED_SESSION_STRATEGY_REF = "expired-session-strategy-ref";
 		final String ATT_SESSION_REGISTRY_ALIAS = "session-registry-alias";
 		final String ATT_SESSION_REGISTRY_REF = "session-registry-ref";
 
@@ -489,10 +502,20 @@ class HttpConfigurationBuilder {
 		filterBuilder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
 
 		String expiryUrl = element.getAttribute(ATT_EXPIRY_URL);
+		String expiredSessionStrategyRef = element.getAttribute(ATT_EXPIRED_SESSION_STRATEGY_REF);
+
+		if (StringUtils.hasText(expiryUrl) && StringUtils.hasText(expiredSessionStrategyRef)) {
+			pc.getReaderContext().error("Cannot use 'expired-url' attribute and 'expired-session-strategy-ref'" +
+					" attribute together.", source);
+		}
 
 		if (StringUtils.hasText(expiryUrl)) {
-			WebConfigUtils.validateHttpRedirect(expiryUrl, pc, source);
-			filterBuilder.addConstructorArgValue(expiryUrl);
+			BeanDefinitionBuilder expiredSessionBldr = BeanDefinitionBuilder
+					.rootBeanDefinition(SimpleRedirectExpiredSessionStrategy.class);
+			expiredSessionBldr.addConstructorArgValue(expiryUrl);
+			filterBuilder.addPropertyValue("expiredSessionStrategy", expiredSessionBldr.getBeanDefinition());
+		} else if (StringUtils.hasText(expiredSessionStrategyRef)) {
+			filterBuilder.addPropertyReference("expiredSessionStrategy", expiredSessionStrategyRef);
 		}
 
 		pc.popAndRegisterContainingComponent();

+ 6 - 0
config/src/main/resources/org/springframework/security/config/spring-security-4.1.rnc

@@ -535,6 +535,9 @@ session-management.attlist &=
 session-management.attlist &=
 	## The URL to which a user will be redirected if they submit an invalid session indentifier. Typically used to detect session timeouts.
 	attribute invalid-session-url {xsd:token}?
+session-management.attlist &=
+	## Allows injection of the InvalidSessionStrategy instance used by the SessionManagementFilter
+	attribute invalid-session-strategy-ref {xsd:token}?
 session-management.attlist &=
 	## Allows injection of the SessionAuthenticationStrategy instance used by the SessionManagementFilter
 	attribute session-authentication-strategy-ref {xsd:token}?
@@ -553,6 +556,9 @@ concurrency-control.attlist &=
 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}?
+concurrency-control.attlist &=
+	## Allows injection of the ExpiredSessionStrategy instance used by the ConcurrentSessionFilter
+	attribute expired-session-strategy-ref {xsd:token}?
 concurrency-control.attlist &=
 	## Specifies that an unauthorized error should be reported when a user attempts to login when they already have the maximum configured sessions open. The default behaviour is to expire the original session. If the session-authentication-error-url attribute is set on the session-management URL, the user will be redirected to this URL.
 	attribute error-if-maximum-exceeded {xsd:boolean}?

+ 14 - 0
config/src/main/resources/org/springframework/security/config/spring-security-4.1.xsd

@@ -1743,6 +1743,13 @@
                 </xs:documentation>
          </xs:annotation>
       </xs:attribute>
+      <xs:attribute name="invalid-session-strategy-ref" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>Allows injection of the InvalidSessionStrategy instance used by the
+                SessionManagementFilter
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
       <xs:attribute name="session-authentication-strategy-ref" type="xs:token">
          <xs:annotation>
             <xs:documentation>Allows injection of the SessionAuthenticationStrategy instance used by the
@@ -1777,6 +1784,13 @@
                 </xs:documentation>
          </xs:annotation>
       </xs:attribute>
+      <xs:attribute name="expired-session-strategy-ref" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>Allows injection of the ExpiredSessionStrategy instance used by the
+                ConcurrentSessionFilter
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
       <xs:attribute name="error-if-maximum-exceeded" type="xs:boolean">
          <xs:annotation>
             <xs:documentation>Specifies that an unauthorized error should be reported when a user attempts to login when

+ 1 - 1
config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceSessionManagementTests.groovy

@@ -67,7 +67,7 @@ class NamespaceSessionManagementTests extends BaseSpringSpec {
 			concurrentStrategy.maximumSessions == 1
 			concurrentStrategy.exceptionIfMaximumExceeded
 			concurrentStrategy.sessionRegistry == CustomSessionManagementConfig.SR
-			findFilter(ConcurrentSessionFilter).expiredUrl == "/expired-session"
+			findFilter(ConcurrentSessionFilter).expiredSessionStrategy.destinationUrl == "/expired-session"
 	}
 
 	@EnableWebSecurity

+ 9 - 10
config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy

@@ -15,13 +15,6 @@
  */
 package org.springframework.security.config.http
 
-import static org.junit.Assert.assertSame
-import static org.mockito.Mockito.*
-
-import javax.servlet.http.HttpServletRequest
-import javax.servlet.http.HttpServletResponse
-
-import org.mockito.Mockito
 import org.springframework.mock.web.MockFilterChain
 import org.springframework.mock.web.MockHttpServletRequest
 import org.springframework.mock.web.MockHttpServletResponse
@@ -41,7 +34,6 @@ import org.springframework.security.web.authentication.logout.CookieClearingLogo
 import org.springframework.security.web.authentication.logout.LogoutFilter
 import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler
 import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter
-import org.springframework.security.web.authentication.session.SessionAuthenticationException
 import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy
 import org.springframework.security.web.context.NullSecurityContextRepository
 import org.springframework.security.web.context.SaveContextOnUpdateOrErrorResponseWrapper
@@ -50,6 +42,13 @@ import org.springframework.security.web.savedrequest.RequestCacheAwareFilter
 import org.springframework.security.web.session.ConcurrentSessionFilter
 import org.springframework.security.web.session.SessionManagementFilter
 
+import javax.servlet.http.HttpServletRequest
+import javax.servlet.http.HttpServletResponse
+
+import static org.junit.Assert.assertSame
+import static org.mockito.Matchers.any
+import static org.mockito.Mockito.verify
+
 /**
  * Tests session-related functionality for the &lt;http&gt; namespace element and &lt;session-management&gt;
  *
@@ -164,7 +163,7 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests {
 
 		then:
 		concurrentSessionFilter instanceof ConcurrentSessionFilter
-		concurrentSessionFilter.expiredUrl == '/expired'
+		concurrentSessionFilter.expiredSessionStrategy.destinationUrl == '/expired'
 		appContext.getBean("sr") != null
 		getFilter(SessionManagementFilter.class) != null
 		sessionRegistryIsValid();
@@ -271,7 +270,7 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests {
 		List filters = getFilters("/someurl");
 
 		expect:
-		filters.get(1).expiredUrl == null
+		filters.get(1).expiredSessionStrategy == null
 	}
 
 	def externalSessionStrategyIsSupported() {

+ 6 - 0
docs/manual/src/docs/asciidoc/index.adoc

@@ -8592,6 +8592,9 @@ Session-management related functionality is implemented by the addition of a `Se
 * **invalid-session-url**
 Setting this attribute will inject the `SessionManagementFilter` with a `SimpleRedirectInvalidSessionStrategy` configured with the attribute value. When an invalid session ID is submitted, the strategy will be invoked, redirecting to the configured URL.
 
+[[nsa-session-management-invalid-session-strategy-ref]]
+* **invalid-session-url**
+Allows injection of the InvalidSessionStrategy instance used by the SessionManagementFilter. Use either this or the `invalid-session-url` attribute but not both.
 
 [[nsa-session-management-session-authentication-error-url]]
 * **session-authentication-error-url**
@@ -8646,6 +8649,9 @@ If set to "true" a `SessionAuthenticationException` will be raised when a user a
 * **expired-url**
 The URL a user will be redirected to if they attempt to use a session which has been "expired" by the concurrent session controller because the user has exceeded the number of allowed sessions and has logged in again elsewhere. Should be set unless `exception-if-maximum-exceeded` is set. If no value is supplied, an expiry message will just be written directly back to the response.
 
+[[nsa-concurrency-control-expired-session-strategy-ref]]
+* **expired-url**
+Allows injection of the ExpiredSessionStrategy instance used by the ConcurrentSessionFilter
 
 [[nsa-concurrency-control-max-sessions]]
 * **max-sessions**

+ 14 - 22
web/src/main/java/org/springframework/security/web/session/ConcurrentSessionFilter.java

@@ -17,7 +17,6 @@
 package org.springframework.security.web.session;
 
 import java.io.IOException;
-
 import javax.servlet.FilterChain;
 import javax.servlet.ServletException;
 import javax.servlet.ServletRequest;
@@ -35,7 +34,6 @@ import org.springframework.security.web.RedirectStrategy;
 import org.springframework.security.web.authentication.logout.CompositeLogoutHandler;
 import org.springframework.security.web.authentication.logout.LogoutHandler;
 import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
-import org.springframework.security.web.util.UrlUtils;
 import org.springframework.util.Assert;
 import org.springframework.web.filter.GenericFilterBean;
 
@@ -51,8 +49,8 @@ import org.springframework.web.filter.GenericFilterBean;
  * as expired. If it has been marked as expired, the configured logout handlers will be
  * called (as happens with
  * {@link org.springframework.security.web.authentication.logout.LogoutFilter}), typically
- * to invalidate the session. A redirect to the expiredURL specified will be performed,
- * and the session invalidation will cause an
+ * to invalidate the session. To handle the expired session a call to the {@link ExpiredSessionStrategy} is made.
+ * The session invalidation will cause an
  * {@link org.springframework.security.web.session.HttpSessionDestroyedEvent} to be
  * published via the
  * {@link org.springframework.security.web.session.HttpSessionEventPublisher} registered
@@ -61,15 +59,15 @@ import org.springframework.web.filter.GenericFilterBean;
  *
  * @author Ben Alex
  * @author Eddú Meléndez
+ * @author Marten Deinum
  */
 public class ConcurrentSessionFilter extends GenericFilterBean {
 	// ~ Instance fields
 	// ================================================================================================
 
-	private SessionRegistry sessionRegistry;
-	private String expiredUrl;
-	private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
+	private final SessionRegistry sessionRegistry;
 	private LogoutHandler handlers = new CompositeLogoutHandler(new SecurityContextLogoutHandler());
+	private ExpiredSessionStrategy expiredSessionStrategy;
 
 	// ~ Methods
 	// ========================================================================================================
@@ -81,17 +79,13 @@ public class ConcurrentSessionFilter extends GenericFilterBean {
 
 	public ConcurrentSessionFilter(SessionRegistry sessionRegistry, String expiredUrl) {
 		Assert.notNull(sessionRegistry, "SessionRegistry required");
-		Assert.isTrue(expiredUrl == null || UrlUtils.isValidRedirectUrl(expiredUrl),
-				expiredUrl + " isn't a valid redirect URL");
 		this.sessionRegistry = sessionRegistry;
-		this.expiredUrl = expiredUrl;
+		this.expiredSessionStrategy = new SimpleRedirectExpiredSessionStrategy(expiredUrl);
 	}
 
 	@Override
 	public void afterPropertiesSet() {
 		Assert.notNull(sessionRegistry, "SessionRegistry required");
-		Assert.isTrue(expiredUrl == null || UrlUtils.isValidRedirectUrl(expiredUrl),
-				expiredUrl + " isn't a valid redirect URL");
 	}
 
 	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
@@ -108,12 +102,14 @@ public class ConcurrentSessionFilter extends GenericFilterBean {
 			if (info != null) {
 				if (info.isExpired()) {
 					// Expired - abort processing
+					if (logger.isDebugEnabled()) {
+						logger.debug("Requested session ID "
+								+ request.getRequestedSessionId() + " has expired.");
+					}
 					doLogout(request, response);
 
-					String targetUrl = determineExpiredUrl(request, info);
-
-					if (targetUrl != null) {
-						redirectStrategy.sendRedirect(request, response, targetUrl);
+					if (this.expiredSessionStrategy != null) {
+						this.expiredSessionStrategy.onExpiredSessionDetected(request, response);
 
 						return;
 					}
@@ -136,10 +132,6 @@ public class ConcurrentSessionFilter extends GenericFilterBean {
 		chain.doFilter(request, response);
 	}
 
-	protected String determineExpiredUrl(HttpServletRequest request,
-			SessionInformation info) {
-		return expiredUrl;
-	}
 
 	private void doLogout(HttpServletRequest request, HttpServletResponse response) {
 		Authentication auth = SecurityContextHolder.getContext().getAuthentication();
@@ -151,7 +143,7 @@ public class ConcurrentSessionFilter extends GenericFilterBean {
 		this.handlers = new CompositeLogoutHandler(handlers);
 	}
 
-	public void setRedirectStrategy(RedirectStrategy redirectStrategy) {
-		this.redirectStrategy = redirectStrategy;
+	public void setExpiredSessionStrategy(ExpiredSessionStrategy expiredSessionStrategy) {
+		this.expiredSessionStrategy=expiredSessionStrategy;
 	}
 }

+ 35 - 0
web/src/main/java/org/springframework/security/web/session/ExpiredSessionStrategy.java

@@ -0,0 +1,35 @@
+/*
+ * Copyright 2015-2016 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
+ *
+ *      http://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.io.IOException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Determines the behaviour of the {@code ConcurrentSessionFilter} when an expired session
+ * is detected in the {@code ConcurrentSessionFilter}.
+ *
+ * @author Marten Deinum
+ * @since 4.1.0
+ */
+public interface ExpiredSessionStrategy {
+
+	void onExpiredSessionDetected(HttpServletRequest request, HttpServletResponse response)
+			throws IOException, ServletException;
+
+}

+ 58 - 0
web/src/main/java/org/springframework/security/web/session/SimpleRedirectExpiredSessionStrategy.java

@@ -0,0 +1,58 @@
+/*
+ * Copyright 2002-2016 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
+ *
+ *      http://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.io.IOException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.security.web.DefaultRedirectStrategy;
+import org.springframework.security.web.RedirectStrategy;
+import org.springframework.security.web.util.UrlUtils;
+import org.springframework.util.Assert;
+
+/**
+ * Performs a redirect to a fixed URL when an expired session is detected by the
+ * {@code ConcurrentSessionFilter}.
+ *
+ * @author Marten Deinum
+ * @since 4.1.0
+ */
+public final class SimpleRedirectExpiredSessionStrategy implements ExpiredSessionStrategy {
+	private final Log logger = LogFactory.getLog(getClass());
+	private final String destinationUrl;
+	private final RedirectStrategy redirectStrategy;
+
+	public SimpleRedirectExpiredSessionStrategy(String invalidSessionUrl) {
+		this(invalidSessionUrl, new DefaultRedirectStrategy());
+	}
+
+	public SimpleRedirectExpiredSessionStrategy(String invalidSessionUrl, RedirectStrategy redirectStrategy) {
+		Assert.isTrue(UrlUtils.isValidRedirectUrl(invalidSessionUrl),
+				"url must start with '/' or with 'http(s)'");
+		this.destinationUrl=invalidSessionUrl;
+		this.redirectStrategy=redirectStrategy;
+	}
+
+	public void onExpiredSessionDetected(HttpServletRequest request,
+			HttpServletResponse response) throws IOException {
+		logger.debug("Redirecting to '" + destinationUrl + "'");
+		redirectStrategy.sendRedirect(request, response, destinationUrl);
+	}
+
+}

+ 12 - 17
web/src/test/java/org/springframework/security/web/concurrent/ConcurrentSessionFilterTests.java

@@ -16,11 +16,7 @@
 
 package org.springframework.security.web.concurrent;
 
-import static org.assertj.core.api.Assertions.*;
-import static org.mockito.Mockito.*;
-
 import java.util.Date;
-
 import javax.servlet.FilterChain;
 
 import org.junit.Test;
@@ -29,10 +25,13 @@ import org.springframework.mock.web.MockHttpServletResponse;
 import org.springframework.mock.web.MockHttpSession;
 import org.springframework.security.core.session.SessionRegistry;
 import org.springframework.security.core.session.SessionRegistryImpl;
-import org.springframework.security.web.DefaultRedirectStrategy;
 import org.springframework.security.web.authentication.logout.LogoutHandler;
 import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
 import org.springframework.security.web.session.ConcurrentSessionFilter;
+import org.springframework.security.web.session.SimpleRedirectExpiredSessionStrategy;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.*;
 
 /**
  * Tests {@link ConcurrentSessionFilter}.
@@ -56,9 +55,10 @@ public class ConcurrentSessionFilterTests {
 		registry.getSessionInformation(session.getId()).expireNow();
 
 		// Setup our test fixture and registry to want this session to be expired
-		ConcurrentSessionFilter filter = new ConcurrentSessionFilter(registry,
-				"/expired.jsp");
-		filter.setRedirectStrategy(new DefaultRedirectStrategy());
+
+		SimpleRedirectExpiredSessionStrategy expiredSessionStrategy = new SimpleRedirectExpiredSessionStrategy("/expired.jsp");
+		ConcurrentSessionFilter filter = new ConcurrentSessionFilter(registry);
+		filter.setExpiredSessionStrategy(expiredSessionStrategy);
 		filter.setLogoutHandlers(new LogoutHandler[] { new SecurityContextLogoutHandler() });
 		filter.afterPropertiesSet();
 
@@ -97,11 +97,6 @@ public class ConcurrentSessionFilterTests {
 		new ConcurrentSessionFilter(null);
 	}
 
-	@Test(expected = IllegalArgumentException.class)
-	public void detectsInvalidUrl() throws Exception {
-		new ConcurrentSessionFilter(new SessionRegistryImpl(), "ImNotValid");
-	}
-
 	@Test
 	public void lastRequestTimeUpdatesCorrectly() throws Exception {
 		// Setup our HTTP request
@@ -115,11 +110,11 @@ public class ConcurrentSessionFilterTests {
 		// Setup our test fixture
 		SessionRegistry registry = new SessionRegistryImpl();
 		registry.registerNewSession(session.getId(), "principal");
-		ConcurrentSessionFilter filter = new ConcurrentSessionFilter(registry,
-				"/expired.jsp");
+		SimpleRedirectExpiredSessionStrategy expiredSessionStrategy = new SimpleRedirectExpiredSessionStrategy("/expired.jsp");
+		ConcurrentSessionFilter filter = new ConcurrentSessionFilter(registry);
+		filter.setExpiredSessionStrategy(expiredSessionStrategy);
 
-		Date lastRequest = registry.getSessionInformation(session.getId())
-				.getLastRequest();
+		Date lastRequest = registry.getSessionInformation(session.getId()).getLastRequest();
 
 		Thread.sleep(1000);