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
This commit is contained in:
Rob Winch 2016-09-15 14:30:52 -05:00
parent 67c9f12964
commit 6650429283
14 changed files with 458 additions and 79 deletions

View File

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

View File

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

View File

@ -535,9 +535,6 @@ session-management.attlist &=
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. ## 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}? 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 &= session-management.attlist &=
## Allows injection of the SessionAuthenticationStrategy instance used by the SessionManagementFilter ## Allows injection of the SessionAuthenticationStrategy instance used by the SessionManagementFilter
attribute session-authentication-strategy-ref {xsd:token}? attribute session-authentication-strategy-ref {xsd:token}?
@ -556,9 +553,6 @@ concurrency-control.attlist &=
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. ## 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}? 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 &= 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. ## 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}? attribute error-if-maximum-exceeded {xsd:boolean}?

View File

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

View File

@ -535,6 +535,9 @@ session-management.attlist &=
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. ## 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}? 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 &= session-management.attlist &=
## Allows injection of the SessionAuthenticationStrategy instance used by the SessionManagementFilter ## Allows injection of the SessionAuthenticationStrategy instance used by the SessionManagementFilter
attribute session-authentication-strategy-ref {xsd:token}? attribute session-authentication-strategy-ref {xsd:token}?
@ -553,6 +556,9 @@ concurrency-control.attlist &=
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. ## 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}? 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 &= 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. ## 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}? attribute error-if-maximum-exceeded {xsd:boolean}?

View File

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

View File

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

View File

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

View File

@ -17,6 +17,7 @@
package org.springframework.security.web.session; package org.springframework.security.web.session;
import java.io.IOException; import java.io.IOException;
import javax.servlet.FilterChain; import javax.servlet.FilterChain;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.ServletRequest; 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.context.SecurityContextHolder;
import org.springframework.security.core.session.SessionInformation; import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry; import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy; import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.logout.CompositeLogoutHandler; import org.springframework.security.web.authentication.logout.CompositeLogoutHandler;
import org.springframework.security.web.authentication.logout.LogoutHandler; import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.web.filter.GenericFilterBean; 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 * as expired. If it has been marked as expired, the configured logout handlers will be
* called (as happens with * called (as happens with
* {@link org.springframework.security.web.authentication.logout.LogoutFilter}), typically * {@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 * The session invalidation will cause an
* {@link org.springframework.security.web.session.HttpSessionDestroyedEvent} to be * {@link org.springframework.security.web.session.HttpSessionDestroyedEvent} to be
* published via the * published via the
@ -62,12 +63,15 @@ import org.springframework.web.filter.GenericFilterBean;
* @author Marten Deinum * @author Marten Deinum
*/ */
public class ConcurrentSessionFilter extends GenericFilterBean { public class ConcurrentSessionFilter extends GenericFilterBean {
// ~ Instance fields // ~ Instance fields
// ================================================================================================ // ================================================================================================
private final SessionRegistry sessionRegistry; private final SessionRegistry sessionRegistry;
private String expiredUrl;
private RedirectStrategy redirectStrategy;
private LogoutHandler handlers = new CompositeLogoutHandler(new SecurityContextLogoutHandler()); private LogoutHandler handlers = new CompositeLogoutHandler(new SecurityContextLogoutHandler());
private ExpiredSessionStrategy expiredSessionStrategy; private SessionInformationExpiredStrategy sessionInformationExpiredStrategy;
// ~ Methods // ~ Methods
// ======================================================================================================== // ========================================================================================================
@ -75,12 +79,42 @@ public class ConcurrentSessionFilter extends GenericFilterBean {
public ConcurrentSessionFilter(SessionRegistry sessionRegistry) { public ConcurrentSessionFilter(SessionRegistry sessionRegistry) {
Assert.notNull(sessionRegistry, "SessionRegistry required"); Assert.notNull(sessionRegistry, "SessionRegistry required");
this.sessionRegistry = sessionRegistry; 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) { public ConcurrentSessionFilter(SessionRegistry sessionRegistry, String expiredUrl) {
Assert.notNull(sessionRegistry, "SessionRegistry required"); 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.sessionRegistry = sessionRegistry;
this.expiredSessionStrategy = new SimpleRedirectExpiredSessionStrategy(expiredUrl); 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.sessionInformationExpiredStrategy = sessionInformationExpiredStrategy;
} }
@Override @Override
@ -108,18 +142,7 @@ public class ConcurrentSessionFilter extends GenericFilterBean {
} }
doLogout(request, response); doLogout(request, response);
if (this.expiredSessionStrategy != null) { this.sessionInformationExpiredStrategy.onExpiredSessionDetected(new SessionInformationExpiredEvent(info, request, response));
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();
}
return; return;
} }
else { else {
@ -132,6 +155,17 @@ public class ConcurrentSessionFilter extends GenericFilterBean {
chain.doFilter(request, response); 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) { private void doLogout(HttpServletRequest request, HttpServletResponse response) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication(); Authentication auth = SecurityContextHolder.getContext().getAuthentication();
@ -143,7 +177,30 @@ public class ConcurrentSessionFilter extends GenericFilterBean {
this.handlers = new CompositeLogoutHandler(handlers); 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();
}
} }
} }

View File

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

View File

@ -16,20 +16,19 @@
package org.springframework.security.web.session; package org.springframework.security.web.session;
import java.io.IOException; import java.io.IOException;
import javax.servlet.ServletException; 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 * Determines the behaviour of the {@code ConcurrentSessionFilter} when an expired session
* is detected in the {@code ConcurrentSessionFilter}. * is detected in the {@code ConcurrentSessionFilter}.
* *
* @author Marten Deinum * @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; throws IOException, ServletException;
} }

View File

@ -16,11 +16,10 @@
package org.springframework.security.web.session; package org.springframework.security.web.session;
import java.io.IOException; 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.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.security.web.DefaultRedirectStrategy; import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy; import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.util.UrlUtils; import org.springframework.security.web.util.UrlUtils;
@ -31,28 +30,27 @@ import org.springframework.util.Assert;
* {@code ConcurrentSessionFilter}. * {@code ConcurrentSessionFilter}.
* *
* @author Marten Deinum * @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 Log logger = LogFactory.getLog(getClass());
private final String destinationUrl; private final String destinationUrl;
private final RedirectStrategy redirectStrategy; private final RedirectStrategy redirectStrategy;
public SimpleRedirectExpiredSessionStrategy(String invalidSessionUrl) { public SimpleRedirectSessionInformationExpiredStrategy(String invalidSessionUrl) {
this(invalidSessionUrl, new DefaultRedirectStrategy()); this(invalidSessionUrl, new DefaultRedirectStrategy());
} }
public SimpleRedirectExpiredSessionStrategy(String invalidSessionUrl, RedirectStrategy redirectStrategy) { public SimpleRedirectSessionInformationExpiredStrategy(String invalidSessionUrl, RedirectStrategy redirectStrategy) {
Assert.isTrue(UrlUtils.isValidRedirectUrl(invalidSessionUrl), Assert.isTrue(UrlUtils.isValidRedirectUrl(invalidSessionUrl),
"url must start with '/' or with 'http(s)'"); "url must start with '/' or with 'http(s)'");
this.destinationUrl=invalidSessionUrl; this.destinationUrl=invalidSessionUrl;
this.redirectStrategy=redirectStrategy; this.redirectStrategy=redirectStrategy;
} }
public void onExpiredSessionDetected(HttpServletRequest request, public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException {
HttpServletResponse response) throws IOException {
logger.debug("Redirecting to '" + destinationUrl + "'"); logger.debug("Redirecting to '" + destinationUrl + "'");
redirectStrategy.sendRedirect(request, response, destinationUrl); redirectStrategy.sendRedirect(event.getRequest(), event.getResponse(), destinationUrl);
} }
} }

View File

@ -17,21 +17,37 @@
package org.springframework.security.web.concurrent; package org.springframework.security.web.concurrent;
import java.util.Date; import java.util.Date;
import javax.servlet.FilterChain;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import org.junit.After;
import org.junit.Test; import org.junit.Test;
import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockHttpSession; 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.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl; 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.LogoutHandler;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.security.web.session.ConcurrentSessionFilter; 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.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}. * Tests {@link ConcurrentSessionFilter}.
@ -41,6 +57,33 @@ import static org.mockito.Mockito.*;
*/ */
public class ConcurrentSessionFilterTests { 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 @Test
public void detectsExpiredSessions() throws Exception { public void detectsExpiredSessions() throws Exception {
// Setup our HTTP request // Setup our HTTP request
@ -56,9 +99,8 @@ public class ConcurrentSessionFilterTests {
// Setup our test fixture and registry to want this session to be expired // Setup our test fixture and registry to want this session to be expired
SimpleRedirectExpiredSessionStrategy expiredSessionStrategy = new SimpleRedirectExpiredSessionStrategy("/expired.jsp"); SimpleRedirectSessionInformationExpiredStrategy expiredSessionStrategy = new SimpleRedirectSessionInformationExpiredStrategy("/expired.jsp");
ConcurrentSessionFilter filter = new ConcurrentSessionFilter(registry); ConcurrentSessionFilter filter = new ConcurrentSessionFilter(registry, expiredSessionStrategy);
filter.setExpiredSessionStrategy(expiredSessionStrategy);
filter.setLogoutHandlers(new LogoutHandler[] { new SecurityContextLogoutHandler() }); filter.setLogoutHandlers(new LogoutHandler[] { new SecurityContextLogoutHandler() });
filter.afterPropertiesSet(); filter.afterPropertiesSet();
@ -110,9 +152,8 @@ public class ConcurrentSessionFilterTests {
// Setup our test fixture // Setup our test fixture
SessionRegistry registry = new SessionRegistryImpl(); SessionRegistry registry = new SessionRegistryImpl();
registry.registerNewSession(session.getId(), "principal"); registry.registerNewSession(session.getId(), "principal");
SimpleRedirectExpiredSessionStrategy expiredSessionStrategy = new SimpleRedirectExpiredSessionStrategy("/expired.jsp"); SimpleRedirectSessionInformationExpiredStrategy expiredSessionStrategy = new SimpleRedirectSessionInformationExpiredStrategy("/expired.jsp");
ConcurrentSessionFilter filter = new ConcurrentSessionFilter(registry); ConcurrentSessionFilter filter = new ConcurrentSessionFilter(registry, expiredSessionStrategy);
filter.setExpiredSessionStrategy(expiredSessionStrategy);
Date lastRequest = registry.getSessionInformation(session.getId()).getLastRequest(); Date lastRequest = registry.getSessionInformation(session.getId()).getLastRequest();
@ -123,4 +164,165 @@ public class ConcurrentSessionFilterTests {
verify(fc).doFilter(request, response); verify(fc).doFilter(request, response);
assertThat(registry.getSessionInformation(session.getId()).getLastRequest().after(lastRequest)).isTrue(); 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]);
}
} }

View File

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