Преглед изворни кода

Polish SessionInformationExpiredStrategy

* Fix passivity and add tests
* Introduce SessionInformationExpiredEvent as a value object
* Rename ExpiredSessionStrategy to SessionInformationExpiredStrategy
  to account for the need of SessionInformation
* Switch to Constructor Injection
* Move the changes to the xsd to 4.2 xsd instead of 4.1

Issue gh-3808
Rob Winch пре 9 година
родитељ
комит
6650429283
14 измењених фајлова са 457 додато и 78 уклоњено
  1. 17 10
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java
  2. 4 4
      config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java
  3. 0 6
      config/src/main/resources/org/springframework/security/config/spring-security-4.1.rnc
  4. 0 14
      config/src/main/resources/org/springframework/security/config/spring-security-4.1.xsd
  5. 6 0
      config/src/main/resources/org/springframework/security/config/spring-security-4.2.rnc
  6. 14 0
      config/src/main/resources/org/springframework/security/config/spring-security-4.2.xsd
  7. 1 1
      config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceSessionManagementTests.groovy
  8. 2 2
      config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy
  9. 75 18
      web/src/main/java/org/springframework/security/web/session/ConcurrentSessionFilter.java
  10. 68 0
      web/src/main/java/org/springframework/security/web/session/SessionInformationExpiredEvent.java
  11. 5 6
      web/src/main/java/org/springframework/security/web/session/SessionInformationExpiredStrategy.java
  12. 7 9
      web/src/main/java/org/springframework/security/web/session/SimpleRedirectSessionInformationExpiredStrategy.java
  13. 210 8
      web/src/test/java/org/springframework/security/web/concurrent/ConcurrentSessionFilterTests.java
  14. 48 0
      web/src/test/java/org/springframework/security/web/session/SessionInformationExpiredEventTests.java

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

@@ -48,11 +48,11 @@ 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.SessionInformationExpiredStrategy;
 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.session.SimpleRedirectSessionInformationExpiredStrategy;
 import org.springframework.util.Assert;
 
 /**
@@ -99,7 +99,7 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
 	private SessionAuthenticationStrategy sessionAuthenticationStrategy;
 	private SessionAuthenticationStrategy providedSessionAuthenticationStrategy;
 	private InvalidSessionStrategy invalidSessionStrategy;
-	private ExpiredSessionStrategy expiredSessionStrategy;
+	private SessionInformationExpiredStrategy expiredSessionStrategy;
 	private List<SessionAuthenticationStrategy> sessionAuthenticationStrategies = new ArrayList<SessionAuthenticationStrategy>();
 	private SessionRegistry sessionRegistry;
 	private Integer maximumSessions;
@@ -355,7 +355,7 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
 		}
 
 		public ConcurrencyControlConfigurer expiredSessionStrategy(
-				ExpiredSessionStrategy expiredSessionStrategy) {
+				SessionInformationExpiredStrategy expiredSessionStrategy) {
 			SessionManagementConfigurer.this.expiredSessionStrategy = expiredSessionStrategy;
 			return this;
 		}
@@ -470,16 +470,23 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
 
 		http.addFilter(sessionManagementFilter);
 		if (isConcurrentSessionControlEnabled()) {
-			ConcurrentSessionFilter concurrentSessionFilter = new ConcurrentSessionFilter(
-					getSessionRegistry(http));
-			concurrentSessionFilter
-					.setExpiredSessionStrategy(getExpiredSessionStrategy());
+			ConcurrentSessionFilter concurrentSessionFilter = createConccurencyFilter(http);
 
 			concurrentSessionFilter = postProcess(concurrentSessionFilter);
 			http.addFilter(concurrentSessionFilter);
 		}
 	}
 
+	private ConcurrentSessionFilter createConccurencyFilter(H http) {
+		SessionInformationExpiredStrategy expireStrategy = getExpiredSessionStrategy();
+		SessionRegistry sessionRegistry = getSessionRegistry(http);
+		if(expireStrategy == null) {
+			return new ConcurrentSessionFilter(sessionRegistry);
+		}
+
+		return new ConcurrentSessionFilter(sessionRegistry, expireStrategy);
+	}
+
 	/**
 	 * Gets the {@link InvalidSessionStrategy} to use. If null and
 	 * {@link #invalidSessionUrl} is not null defaults to
@@ -505,7 +512,7 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
 		return this.invalidSessionStrategy;
 	}
 
-	ExpiredSessionStrategy getExpiredSessionStrategy() {
+	SessionInformationExpiredStrategy getExpiredSessionStrategy() {
 		if (this.expiredSessionStrategy != null) {
 			return this.expiredSessionStrategy;
 		}
@@ -515,7 +522,7 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
 		}
 
 		if (this.expiredSessionStrategy == null) {
-			this.expiredSessionStrategy = new SimpleRedirectExpiredSessionStrategy(
+			this.expiredSessionStrategy = new SimpleRedirectSessionInformationExpiredStrategy(
 					this.expiredUrl);
 		}
 		return this.expiredSessionStrategy;

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

@@ -65,7 +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.SimpleRedirectSessionInformationExpiredStrategy;
 import org.springframework.security.web.session.SimpleRedirectInvalidSessionStrategy;
 import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 import org.springframework.util.ClassUtils;
@@ -511,11 +511,11 @@ class HttpConfigurationBuilder {
 
 		if (StringUtils.hasText(expiryUrl)) {
 			BeanDefinitionBuilder expiredSessionBldr = BeanDefinitionBuilder
-					.rootBeanDefinition(SimpleRedirectExpiredSessionStrategy.class);
+					.rootBeanDefinition(SimpleRedirectSessionInformationExpiredStrategy.class);
 			expiredSessionBldr.addConstructorArgValue(expiryUrl);
-			filterBuilder.addPropertyValue("expiredSessionStrategy", expiredSessionBldr.getBeanDefinition());
+			filterBuilder.addConstructorArgValue(expiredSessionBldr.getBeanDefinition());
 		} else if (StringUtils.hasText(expiredSessionStrategyRef)) {
-			filterBuilder.addPropertyReference("expiredSessionStrategy", expiredSessionStrategyRef);
+			filterBuilder.addConstructorArgReference(expiredSessionStrategyRef);
 		}
 
 		pc.popAndRegisterContainingComponent();

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

@@ -535,9 +535,6 @@ 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}?
@@ -556,9 +553,6 @@ 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}?

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

@@ -1743,13 +1743,6 @@
                 </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
@@ -1784,13 +1777,6 @@
                 </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

+ 6 - 0
config/src/main/resources/org/springframework/security/config/spring-security-4.2.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 SessionInformationExpiredStrategy 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.2.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 SessionInformationExpiredStrategy 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).expiredSessionStrategy.destinationUrl == "/expired-session"
+			findFilter(ConcurrentSessionFilter).sessionInformationExpiredStrategy.destinationUrl == "/expired-session"
 	}
 
 	@EnableWebSecurity

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

@@ -163,7 +163,7 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests {
 
 		then:
 		concurrentSessionFilter instanceof ConcurrentSessionFilter
-		concurrentSessionFilter.expiredSessionStrategy.destinationUrl == '/expired'
+		concurrentSessionFilter.sessionInformationExpiredStrategy.destinationUrl == '/expired'
 		appContext.getBean("sr") != null
 		getFilter(SessionManagementFilter.class) != null
 		sessionRegistryIsValid();
@@ -270,7 +270,7 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests {
 		List filters = getFilters("/someurl");
 
 		expect:
-		filters.get(1).expiredSessionStrategy == null
+		filters.get(1).sessionInformationExpiredStrategy.class.name == 'org.springframework.security.web.session.ConcurrentSessionFilter$ResponseBodySessionInformationExpiredStrategy'
 	}
 
 	def externalSessionStrategyIsSupported() {

+ 75 - 18
web/src/main/java/org/springframework/security/web/session/ConcurrentSessionFilter.java

@@ -17,6 +17,7 @@
 package org.springframework.security.web.session;
 
 import java.io.IOException;
+
 import javax.servlet.FilterChain;
 import javax.servlet.ServletException;
 import javax.servlet.ServletRequest;
@@ -29,11 +30,11 @@ import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.core.session.SessionInformation;
 import org.springframework.security.core.session.SessionRegistry;
-import org.springframework.security.web.DefaultRedirectStrategy;
 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;
 
@@ -49,7 +50,7 @@ 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. To handle the expired session a call to the {@link ExpiredSessionStrategy} is made.
+ * to invalidate the session. To handle the expired session a call to the {@link SessionInformationExpiredStrategy} is made.
  * The session invalidation will cause an
  * {@link org.springframework.security.web.session.HttpSessionDestroyedEvent} to be
  * published via the
@@ -62,12 +63,15 @@ import org.springframework.web.filter.GenericFilterBean;
  * @author Marten Deinum
  */
 public class ConcurrentSessionFilter extends GenericFilterBean {
+
 	// ~ Instance fields
 	// ================================================================================================
 
 	private final SessionRegistry sessionRegistry;
+	private String expiredUrl;
+	private RedirectStrategy redirectStrategy;
 	private LogoutHandler handlers = new CompositeLogoutHandler(new SecurityContextLogoutHandler());
-	private ExpiredSessionStrategy expiredSessionStrategy;
+	private SessionInformationExpiredStrategy sessionInformationExpiredStrategy;
 
 	// ~ Methods
 	// ========================================================================================================
@@ -75,12 +79,42 @@ public class ConcurrentSessionFilter extends GenericFilterBean {
 	public ConcurrentSessionFilter(SessionRegistry sessionRegistry) {
 		Assert.notNull(sessionRegistry, "SessionRegistry required");
 		this.sessionRegistry = sessionRegistry;
+		this.sessionInformationExpiredStrategy = new ResponseBodySessionInformationExpiredStrategy();
 	}
 
+	/**
+	 * Creates a new instance
+	 *
+	 * @param sessionRegistry the SessionRegistry to use
+	 * @param expiredUrl the URL to redirect to
+	 * @deprecated use {@link #ConcurrentSessionFilter(SessionRegistry, SessionInformationExpiredStrategy)} with {@link SimpleRedirectSessionInformationExpiredStrategy} instead.
+	 */
+	@Deprecated
 	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.expiredUrl = expiredUrl;
+		this.sessionRegistry = sessionRegistry;
+		this.sessionInformationExpiredStrategy = new SessionInformationExpiredStrategy() {
+
+			@Override
+			public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
+				HttpServletRequest request = event.getRequest();
+				HttpServletResponse response = event.getResponse();
+				SessionInformation info = event.getSessionInformation();
+
+				redirectStrategy.sendRedirect(request, response, determineExpiredUrl(request, info));
+			}
+
+		};
+	}
+
+	public ConcurrentSessionFilter(SessionRegistry sessionRegistry, SessionInformationExpiredStrategy sessionInformationExpiredStrategy) {
+		Assert.notNull(sessionRegistry, "sessionRegistry required");
+		Assert.notNull(sessionInformationExpiredStrategy, "sessionInformationExpiredStrategy cannot be null");
 		this.sessionRegistry = sessionRegistry;
-		this.expiredSessionStrategy = new SimpleRedirectExpiredSessionStrategy(expiredUrl);
+		this.sessionInformationExpiredStrategy = sessionInformationExpiredStrategy;
 	}
 
 	@Override
@@ -108,18 +142,7 @@ public class ConcurrentSessionFilter extends GenericFilterBean {
 					}
 					doLogout(request, response);
 
-					if (this.expiredSessionStrategy != null) {
-						this.expiredSessionStrategy.onExpiredSessionDetected(request, response);
-
-						return;
-					}
-					else {
-						response.getWriter().print(
-								"This session has been expired (possibly due to multiple concurrent "
-										+ "logins being attempted as the same user).");
-						response.flushBuffer();
-					}
-
+					this.sessionInformationExpiredStrategy.onExpiredSessionDetected(new SessionInformationExpiredEvent(info, request, response));
 					return;
 				}
 				else {
@@ -132,6 +155,17 @@ public class ConcurrentSessionFilter extends GenericFilterBean {
 		chain.doFilter(request, response);
 	}
 
+	/**
+	 * Determine the URL for expiration
+	 * @param request the HttpServletRequest
+	 * @param info the {@link SessionInformation}
+	 * @return the URL for expiration
+	 * @deprecated Use {@link #ConcurrentSessionFilter(SessionRegistry, SessionInformationExpiredStrategy)} instead.
+	 */
+	protected String determineExpiredUrl(HttpServletRequest request,
+			SessionInformation info) {
+		return expiredUrl;
+	}
 
 	private void doLogout(HttpServletRequest request, HttpServletResponse response) {
 		Authentication auth = SecurityContextHolder.getContext().getAuthentication();
@@ -143,7 +177,30 @@ public class ConcurrentSessionFilter extends GenericFilterBean {
 		this.handlers = new CompositeLogoutHandler(handlers);
 	}
 
-	public void setExpiredSessionStrategy(ExpiredSessionStrategy expiredSessionStrategy) {
-		this.expiredSessionStrategy=expiredSessionStrategy;
+	/**
+	 * Sets the {@link RedirectStrategy} used with {@link #ConcurrentSessionFilter(SessionRegistry, String)}
+	 * @param redirectStrategy the {@link RedirectStrategy} to use
+	 * @deprecated use {@link #ConcurrentSessionFilter(SessionRegistry, SessionInformationExpiredStrategy)} instead.
+	 */
+	public void setRedirectStrategy(RedirectStrategy redirectStrategy) {
+		this.redirectStrategy = redirectStrategy;
+	}
+
+	/**
+	 * A {@link SessionInformationExpiredStrategy} that writes an error message to the response body.
+	 * @author Rob Winch
+	 * @since 4.2
+	 */
+	private static final class ResponseBodySessionInformationExpiredStrategy
+			implements SessionInformationExpiredStrategy {
+		@Override
+		public void onExpiredSessionDetected(SessionInformationExpiredEvent event)
+				throws IOException, ServletException {
+			HttpServletResponse response = event.getResponse();
+			response.getWriter().print(
+					"This session has been expired (possibly due to multiple concurrent "
+							+ "logins being attempted as the same user).");
+			response.flushBuffer();
+		}
 	}
 }

+ 68 - 0
web/src/main/java/org/springframework/security/web/session/SessionInformationExpiredEvent.java

@@ -0,0 +1,68 @@
+/*
+ * Copyright 2012-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 javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.context.ApplicationEvent;
+import org.springframework.security.core.session.SessionInformation;
+import org.springframework.util.Assert;
+
+/**
+ * An event for when a {@link SessionInformation} is expired.
+ * @author Rob Winch
+ * @since 4.2
+ */
+public final class SessionInformationExpiredEvent extends ApplicationEvent {
+
+	private HttpServletRequest request;
+	private HttpServletResponse response;
+
+	/**
+	 * Creates a new instance
+	 *
+	 * @param sessionInformation the SessionInformation that is expired
+	 * @param request the HttpServletRequest
+	 * @param response the HttpServletResponse
+	 */
+	public SessionInformationExpiredEvent(SessionInformation sessionInformation, HttpServletRequest request, HttpServletResponse response) {
+		super(sessionInformation);
+		Assert.notNull(request, "request cannot be null");
+		Assert.notNull(response, "response cannot be null");
+		this.request = request;
+		this.response = response;
+	}
+
+	/**
+	 * @return the request
+	 */
+	public HttpServletRequest getRequest() {
+		return request;
+	}
+
+	/**
+	 * @return the response
+	 */
+	public HttpServletResponse getResponse() {
+		return response;
+	}
+
+	public SessionInformation getSessionInformation() {
+		return (SessionInformation) getSource();
+	}
+}

+ 5 - 6
web/src/main/java/org/springframework/security/web/session/ExpiredSessionStrategy.java → web/src/main/java/org/springframework/security/web/session/SessionInformationExpiredStrategy.java

@@ -16,20 +16,19 @@
 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
+ * @author Rob Winch
+ * @since 4.2.0
  */
-public interface ExpiredSessionStrategy {
+public interface SessionInformationExpiredStrategy {
 
-	void onExpiredSessionDetected(HttpServletRequest request, HttpServletResponse response)
+	void onExpiredSessionDetected(SessionInformationExpiredEvent eventØ)
 			throws IOException, ServletException;
-
 }

+ 7 - 9
web/src/main/java/org/springframework/security/web/session/SimpleRedirectExpiredSessionStrategy.java → web/src/main/java/org/springframework/security/web/session/SimpleRedirectSessionInformationExpiredStrategy.java

@@ -16,11 +16,10 @@
 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;
@@ -31,28 +30,27 @@ import org.springframework.util.Assert;
  * {@code ConcurrentSessionFilter}.
  *
  * @author Marten Deinum
- * @since 4.1.0
+ * @since 4.2.0
  */
-public final class SimpleRedirectExpiredSessionStrategy implements ExpiredSessionStrategy {
+public final class SimpleRedirectSessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {
 	private final Log logger = LogFactory.getLog(getClass());
 	private final String destinationUrl;
 	private final RedirectStrategy redirectStrategy;
 
-	public SimpleRedirectExpiredSessionStrategy(String invalidSessionUrl) {
+	public SimpleRedirectSessionInformationExpiredStrategy(String invalidSessionUrl) {
 		this(invalidSessionUrl, new DefaultRedirectStrategy());
 	}
 
-	public SimpleRedirectExpiredSessionStrategy(String invalidSessionUrl, RedirectStrategy redirectStrategy) {
+	public SimpleRedirectSessionInformationExpiredStrategy(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 {
+	public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException {
 		logger.debug("Redirecting to '" + destinationUrl + "'");
-		redirectStrategy.sendRedirect(request, response, destinationUrl);
+		redirectStrategy.sendRedirect(event.getRequest(), event.getResponse(), destinationUrl);
 	}
 
 }

+ 210 - 8
web/src/test/java/org/springframework/security/web/concurrent/ConcurrentSessionFilterTests.java

@@ -17,21 +17,37 @@
 package org.springframework.security.web.concurrent;
 
 import java.util.Date;
+
 import javax.servlet.FilterChain;
+import javax.servlet.http.HttpServletRequest;
 
+import org.junit.After;
 import org.junit.Test;
+
+import org.springframework.mock.web.MockFilterChain;
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletResponse;
 import org.springframework.mock.web.MockHttpSession;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.session.SessionInformation;
 import org.springframework.security.core.session.SessionRegistry;
 import org.springframework.security.core.session.SessionRegistryImpl;
+import org.springframework.security.web.RedirectStrategy;
 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 org.springframework.security.web.session.SessionInformationExpiredStrategy;
+import org.springframework.security.web.session.SimpleRedirectSessionInformationExpiredStrategy;
 
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.*;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
 
 /**
  * Tests {@link ConcurrentSessionFilter}.
@@ -41,6 +57,33 @@ import static org.mockito.Mockito.*;
  */
 public class ConcurrentSessionFilterTests {
 
+	@After
+	public void cleanup() {
+		SecurityContextHolder.clearContext();
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void constructorSessionRegistryWhenSessionRegistryNullThenExceptionThrown() {
+		new ConcurrentSessionFilter(null);
+	}
+
+	@SuppressWarnings("deprecation")
+	@Test(expected = IllegalArgumentException.class)
+	public void constructorSessionRegistryExpiresUrlWhenInvalidUrlThenExceptionThrown() {
+		new ConcurrentSessionFilter(new SessionRegistryImpl(), "oops");
+	}
+
+	@SuppressWarnings("deprecation")
+	@Test(expected = IllegalArgumentException.class)
+	public void constructorSessionRegistryExpiresUrlWhenSessionRegistryNullThenExceptionThrown() {
+		new ConcurrentSessionFilter(null, "/expired");
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void constructorSessionRegistrySessionInformationExpiredStrategyWhenStrategyIsNullThenThrowsException() {
+		new ConcurrentSessionFilter(new SessionRegistryImpl(), (SessionInformationExpiredStrategy) null);
+	}
+
 	@Test
 	public void detectsExpiredSessions() throws Exception {
 		// Setup our HTTP request
@@ -56,9 +99,8 @@ public class ConcurrentSessionFilterTests {
 
 		// Setup our test fixture and registry to want this session to be expired
 
-		SimpleRedirectExpiredSessionStrategy expiredSessionStrategy = new SimpleRedirectExpiredSessionStrategy("/expired.jsp");
-		ConcurrentSessionFilter filter = new ConcurrentSessionFilter(registry);
-		filter.setExpiredSessionStrategy(expiredSessionStrategy);
+		SimpleRedirectSessionInformationExpiredStrategy expiredSessionStrategy = new SimpleRedirectSessionInformationExpiredStrategy("/expired.jsp");
+		ConcurrentSessionFilter filter = new ConcurrentSessionFilter(registry, expiredSessionStrategy);
 		filter.setLogoutHandlers(new LogoutHandler[] { new SecurityContextLogoutHandler() });
 		filter.afterPropertiesSet();
 
@@ -110,9 +152,8 @@ public class ConcurrentSessionFilterTests {
 		// Setup our test fixture
 		SessionRegistry registry = new SessionRegistryImpl();
 		registry.registerNewSession(session.getId(), "principal");
-		SimpleRedirectExpiredSessionStrategy expiredSessionStrategy = new SimpleRedirectExpiredSessionStrategy("/expired.jsp");
-		ConcurrentSessionFilter filter = new ConcurrentSessionFilter(registry);
-		filter.setExpiredSessionStrategy(expiredSessionStrategy);
+		SimpleRedirectSessionInformationExpiredStrategy expiredSessionStrategy = new SimpleRedirectSessionInformationExpiredStrategy("/expired.jsp");
+		ConcurrentSessionFilter filter = new ConcurrentSessionFilter(registry, expiredSessionStrategy);
 
 		Date lastRequest = registry.getSessionInformation(session.getId()).getLastRequest();
 
@@ -123,4 +164,165 @@ public class ConcurrentSessionFilterTests {
 		verify(fc).doFilter(request, response);
 		assertThat(registry.getSessionInformation(session.getId()).getLastRequest().after(lastRequest)).isTrue();
 	}
+
+	@Test
+	public void doFilterWhenNoSessionThenChainIsContinued() throws Exception {
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		MockHttpServletResponse response = new MockHttpServletResponse();
+
+		RedirectStrategy redirect = mock(RedirectStrategy.class);
+		SessionRegistry registry = mock(SessionRegistry.class);
+		SessionInformation information = new SessionInformation("user", "sessionId", new Date(System.currentTimeMillis() - 1000));
+		information.expireNow();
+		when(registry.getSessionInformation(anyString())).thenReturn(information);
+
+
+		String expiredUrl = "/expired";
+		ConcurrentSessionFilter filter = new ConcurrentSessionFilter(registry,
+				expiredUrl);
+		filter.setRedirectStrategy(redirect);
+
+		MockFilterChain chain = new MockFilterChain();
+
+		filter.doFilter(request, response, chain);
+
+		assertThat(chain.getRequest()).isNotNull();
+	}
+
+	@Test
+	public void doFilterWhenNoSessionInformationThenChainIsContinued() throws Exception {
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.setSession(new MockHttpSession());
+		MockHttpServletResponse response = new MockHttpServletResponse();
+
+		RedirectStrategy redirect = mock(RedirectStrategy.class);
+		SessionRegistry registry = mock(SessionRegistry.class);
+
+
+		String expiredUrl = "/expired";
+		ConcurrentSessionFilter filter = new ConcurrentSessionFilter(registry,
+				expiredUrl);
+		filter.setRedirectStrategy(redirect);
+
+		MockFilterChain chain = new MockFilterChain();
+
+		filter.doFilter(request, response, chain);
+
+		assertThat(chain.getRequest()).isNotNull();
+	}
+
+	@Test
+	public void doFilterWhenCustomRedirectStrategyThenCustomRedirectStrategyUsed() throws Exception {
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		MockHttpSession session = new MockHttpSession();
+		request.setSession(session);
+		MockHttpServletResponse response = new MockHttpServletResponse();
+
+		RedirectStrategy redirect = mock(RedirectStrategy.class);
+		SessionRegistry registry = mock(SessionRegistry.class);
+		SessionInformation information = new SessionInformation("user", "sessionId", new Date(System.currentTimeMillis() - 1000));
+		information.expireNow();
+		when(registry.getSessionInformation(anyString())).thenReturn(information);
+
+
+		String expiredUrl = "/expired";
+		ConcurrentSessionFilter filter = new ConcurrentSessionFilter(registry,
+				expiredUrl);
+		filter.setRedirectStrategy(redirect);
+
+		filter.doFilter(request, response, new MockFilterChain());
+
+		verify(redirect).sendRedirect(request, response, expiredUrl);
+	}
+
+	@Test
+	public void doFilterWhenOverrideThenCustomRedirectStrategyUsed() throws Exception {
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		MockHttpSession session = new MockHttpSession();
+		request.setSession(session);
+		MockHttpServletResponse response = new MockHttpServletResponse();
+
+		RedirectStrategy redirect = mock(RedirectStrategy.class);
+		SessionRegistry registry = mock(SessionRegistry.class);
+		SessionInformation information = new SessionInformation("user", "sessionId", new Date(System.currentTimeMillis() - 1000));
+		information.expireNow();
+		when(registry.getSessionInformation(anyString())).thenReturn(information);
+
+
+		final String expiredUrl = "/expired";
+		ConcurrentSessionFilter filter = new ConcurrentSessionFilter(registry,
+				expiredUrl + "will-be-overrridden") {
+					/* (non-Javadoc)
+					 * @see org.springframework.security.web.session.ConcurrentSessionFilter#determineExpiredUrl(javax.servlet.http.HttpServletRequest, org.springframework.security.core.session.SessionInformation)
+					 */
+					@Override
+					protected String determineExpiredUrl(HttpServletRequest request,
+							SessionInformation info) {
+						return expiredUrl;
+					}
+
+		};
+		filter.setRedirectStrategy(redirect);
+
+		filter.doFilter(request, response, new MockFilterChain());
+
+		verify(redirect).sendRedirect(request, response, expiredUrl);
+	}
+
+	@Test
+	public void doFilterWhenNoExpiredUrlThenResponseWritten() throws Exception {
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		MockHttpSession session = new MockHttpSession();
+		request.setSession(session);
+		MockHttpServletResponse response = new MockHttpServletResponse();
+
+		SessionRegistry registry = mock(SessionRegistry.class);
+		SessionInformation information = new SessionInformation("user", "sessionId", new Date(System.currentTimeMillis() - 1000));
+		information.expireNow();
+		when(registry.getSessionInformation(anyString())).thenReturn(information);
+
+
+		ConcurrentSessionFilter filter = new ConcurrentSessionFilter(registry);
+
+		filter.doFilter(request, response, new MockFilterChain());
+
+		assertThat(response.getContentAsString()).contains(
+				"This session has been expired (possibly due to multiple concurrent logins being attempted as the same user).");
+	}
+
+	@Test
+	public void doFilterWhenCustomLogoutHandlersThenHandlersUsed() throws Exception {
+		LogoutHandler handler = mock(LogoutHandler.class);
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		MockHttpSession session = new MockHttpSession();
+		request.setSession(session);
+		MockHttpServletResponse response = new MockHttpServletResponse();
+
+		SessionRegistry registry = mock(SessionRegistry.class);
+		SessionInformation information = new SessionInformation("user", "sessionId", new Date(System.currentTimeMillis() - 1000));
+		information.expireNow();
+		when(registry.getSessionInformation(anyString())).thenReturn(information);
+
+
+		ConcurrentSessionFilter filter = new ConcurrentSessionFilter(registry);
+		filter.setLogoutHandlers(new LogoutHandler[] { handler } );
+
+		filter.doFilter(request, response, new MockFilterChain());
+
+		verify(handler).logout(eq(request), eq(response), any(Authentication.class));
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void setLogoutHandlersWhenNullThenThrowsException() {
+		ConcurrentSessionFilter filter = new ConcurrentSessionFilter(new SessionRegistryImpl());
+
+		filter.setLogoutHandlers(null);
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void setLogoutHandlersWhenEmptyThenThrowsException() {
+		ConcurrentSessionFilter filter = new ConcurrentSessionFilter(new SessionRegistryImpl());
+
+		filter.setLogoutHandlers(new LogoutHandler[0]);
+	}
 }

+ 48 - 0
web/src/test/java/org/springframework/security/web/session/SessionInformationExpiredEventTests.java

@@ -0,0 +1,48 @@
+/*
+ * Copyright 2012-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.util.Date;
+
+import org.junit.Test;
+
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.security.core.session.SessionInformation;
+
+/**
+ * @author Rob Winch
+ * @since 4.2
+ */
+public class SessionInformationExpiredEventTests {
+
+	@Test(expected = IllegalArgumentException.class)
+	public void constructorWhenSessionInformationNullThenThrowsException() {
+		new SessionInformationExpiredEvent(null, new MockHttpServletRequest(), new MockHttpServletResponse());
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void constructorWhenRequestNullThenThrowsException() {
+		new SessionInformationExpiredEvent(new SessionInformation("fake", "sessionId", new Date()), null, new MockHttpServletResponse());
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void constructorWhenResponseNullThenThrowsException() {
+		new SessionInformationExpiredEvent(new SessionInformation("fake", "sessionId", new Date()), new MockHttpServletRequest(), null);
+	}
+
+}