SEC-2137: Allow disabling session fixation and enable concurrency control

This commit is contained in:
Rob Winch 2013-08-08 17:10:21 -05:00
parent 867f02e8ac
commit 13da42ca1b
15 changed files with 970 additions and 65 deletions

View File

@ -15,7 +15,11 @@
*/ */
package org.springframework.security.config.annotation.web.configurers; package org.springframework.security.config.annotation.web.configurers;
import java.util.Arrays;
import java.util.List;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@ -23,7 +27,10 @@ import org.springframework.security.config.http.SessionCreationPolicy;
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.authentication.SimpleUrlAuthenticationFailureHandler; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.session.ConcurrentSessionControlStrategy; import org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy;
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy; import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
@ -70,7 +77,8 @@ import org.springframework.util.Assert;
* @see ConcurrentSessionFilter * @see ConcurrentSessionFilter
*/ */
public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractHttpConfigurer<H> { public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractHttpConfigurer<H> {
private SessionAuthenticationStrategy sessionAuthenticationStrategy = new SessionFixationProtectionStrategy(); private SessionAuthenticationStrategy sessionFixationAuthenticationStrategy = new SessionFixationProtectionStrategy();
private SessionAuthenticationStrategy sessionAuthenticationStrategy;
private SessionRegistry sessionRegistry = new SessionRegistryImpl(); private SessionRegistry sessionRegistry = new SessionRegistryImpl();
private Integer maximumSessions; private Integer maximumSessions;
private String expiredUrl; private String expiredUrl;
@ -149,17 +157,25 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
/** /**
* Allows explicitly specifying the {@link SessionAuthenticationStrategy}. * Allows explicitly specifying the {@link SessionAuthenticationStrategy}.
* The default is to use {@link SessionFixationProtectionStrategy}. If * The default is to use {@link SessionFixationProtectionStrategy}. If
* restricting the maximum number of sessions is configured, * restricting the maximum number of sessions is configured, then
* {@link ConcurrentSessionControlStrategy} will be used. * {@link CompositeSessionAuthenticationStrategy} delegating to
* {@link ConcurrentSessionControlAuthenticationStrategy},
* {@link SessionFixationProtectionStrategy} (optional), and
* {@link RegisterSessionAuthenticationStrategy} will be used.
* *
* @param sessionAuthenticationStrategy * @param sessionAuthenticationStrategy
* @return the {@link SessionManagementConfigurer} for further customizations * @return the {@link SessionManagementConfigurer} for further
* customizations
*/ */
public SessionManagementConfigurer<H> sessionAuthenticationStrategy(SessionAuthenticationStrategy sessionAuthenticationStrategy) { public SessionManagementConfigurer<H> sessionAuthenticationStrategy(SessionAuthenticationStrategy sessionAuthenticationStrategy) {
this.sessionAuthenticationStrategy = sessionAuthenticationStrategy; this.sessionFixationAuthenticationStrategy = sessionAuthenticationStrategy;
return this; return this;
} }
public SessionFixationConfigurer sessionFixation() {
return new SessionFixationConfigurer();
}
/** /**
* Controls the maximum number of sessions for a user. The default is to allow any number of users. * Controls the maximum number of sessions for a user. The default is to allow any number of users.
* @param maximumSessions the maximum number of sessions for a user * @param maximumSessions the maximum number of sessions for a user
@ -167,10 +183,57 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
*/ */
public ConcurrencyControlConfigurer maximumSessions(int maximumSessions) { public ConcurrencyControlConfigurer maximumSessions(int maximumSessions) {
this.maximumSessions = maximumSessions; this.maximumSessions = maximumSessions;
this.sessionAuthenticationStrategy = null;
return new ConcurrencyControlConfigurer(); return new ConcurrencyControlConfigurer();
} }
/**
* Allows configuring SessionFixation protection
*
* @author Rob Winch
*/
public final class SessionFixationConfigurer {
/**
* Specifies that a new session should be created, but the session
* attributes from the original {@link HttpSession} should not be
* retained.
*
* @return the {@link SessionManagementConfigurer} for further customizations
*/
public SessionManagementConfigurer<H> newSession() {
SessionFixationProtectionStrategy sessionFixationProtectionStrategy = new SessionFixationProtectionStrategy();
sessionFixationProtectionStrategy.setMigrateSessionAttributes(false);
SessionManagementConfigurer.this.sessionFixationAuthenticationStrategy = sessionFixationProtectionStrategy;
return SessionManagementConfigurer.this;
}
/**
* Specifies that a new session should be created and the session
* attributes from the original {@link HttpSession} should be
* retained.
*
* @return the {@link SessionManagementConfigurer} for further customizations
*/
public SessionManagementConfigurer<H> migrateSession() {
SessionManagementConfigurer.this.sessionFixationAuthenticationStrategy = new SessionFixationProtectionStrategy();
return SessionManagementConfigurer.this;
}
/**
* Specifies that no session fixation protection should be enabled. This
* may be useful when utilizing other mechanisms for protecting against
* session fixation. For example, if application container session
* fixation protection is already in use. Otherwise, this option is not
* recommended.
*
* @return the {@link SessionManagementConfigurer} for further
* customizations
*/
public SessionManagementConfigurer<H> none() {
SessionManagementConfigurer.this.sessionFixationAuthenticationStrategy = new NullAuthenticatedSessionStrategy();
return SessionManagementConfigurer.this;
}
}
/** /**
* Allows configuring controlling of multiple sessions. * Allows configuring controlling of multiple sessions.
* *
@ -314,10 +377,18 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
return sessionAuthenticationStrategy; return sessionAuthenticationStrategy;
} }
if(isConcurrentSessionControlEnabled()) { if(isConcurrentSessionControlEnabled()) {
ConcurrentSessionControlStrategy concurrentSessionControlStrategy = new ConcurrentSessionControlStrategy(sessionRegistry); ConcurrentSessionControlAuthenticationStrategy concurrentSessionControlStrategy = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry);
concurrentSessionControlStrategy.setMaximumSessions(maximumSessions); concurrentSessionControlStrategy.setMaximumSessions(maximumSessions);
concurrentSessionControlStrategy.setExceptionIfMaximumExceeded(maxSessionsPreventsLogin); concurrentSessionControlStrategy.setExceptionIfMaximumExceeded(maxSessionsPreventsLogin);
sessionAuthenticationStrategy = concurrentSessionControlStrategy; concurrentSessionControlStrategy = postProcess(concurrentSessionControlStrategy);
RegisterSessionAuthenticationStrategy registerSessionStrategy = new RegisterSessionAuthenticationStrategy(sessionRegistry);
registerSessionStrategy = postProcess(registerSessionStrategy);
List<SessionAuthenticationStrategy> delegateStrategies = Arrays.asList(concurrentSessionControlStrategy, sessionFixationAuthenticationStrategy, registerSessionStrategy);
sessionAuthenticationStrategy = postProcess(new CompositeSessionAuthenticationStrategy(delegateStrategies));
} else {
sessionAuthenticationStrategy = sessionFixationAuthenticationStrategy;
} }
return sessionAuthenticationStrategy; return sessionAuthenticationStrategy;
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2012 the original author or authors. * Copyright 2002-2013 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -18,14 +18,17 @@ package org.springframework.security.config.http;
import static org.springframework.security.config.http.HttpSecurityBeanDefinitionParser.*; import static org.springframework.security.config.http.HttpSecurityBeanDefinitionParser.*;
import static org.springframework.security.config.http.SecurityFilters.*; import static org.springframework.security.config.http.SecurityFilters.*;
import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import javax.servlet.ServletRequest; import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.BeanMetadataElement; import org.springframework.beans.BeanMetadataElement;
import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanReference; import org.springframework.beans.factory.config.BeanReference;
import org.springframework.beans.factory.config.BeanReferenceFactoryBean;
import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.parsing.BeanComponentDefinition; import org.springframework.beans.factory.parsing.BeanComponentDefinition;
import org.springframework.beans.factory.parsing.CompositeComponentDefinition; import org.springframework.beans.factory.parsing.CompositeComponentDefinition;
@ -51,7 +54,10 @@ import org.springframework.security.web.access.expression.WebExpressionVoter;
import org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource; import org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.session.ConcurrentSessionControlStrategy; import org.springframework.security.web.authentication.session.ChangeSessionIdAuthenticationStrategy;
import org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy; import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.NullSecurityContextRepository; import org.springframework.security.web.context.NullSecurityContextRepository;
@ -66,6 +72,7 @@ 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.SimpleRedirectInvalidSessionStrategy; import org.springframework.security.web.session.SimpleRedirectInvalidSessionStrategy;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils; import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Element; import org.w3c.dom.Element;
@ -83,6 +90,7 @@ class HttpConfigurationBuilder {
private static final String ATT_SESSION_FIXATION_PROTECTION = "session-fixation-protection"; private static final String ATT_SESSION_FIXATION_PROTECTION = "session-fixation-protection";
private static final String OPT_SESSION_FIXATION_NO_PROTECTION = "none"; private static final String OPT_SESSION_FIXATION_NO_PROTECTION = "none";
private static final String OPT_SESSION_FIXATION_MIGRATE_SESSION = "migrateSession"; private static final String OPT_SESSION_FIXATION_MIGRATE_SESSION = "migrateSession";
private static final String OPT_CHANGE_SESSION_ID = "changeSessionId";
private static final String ATT_INVALID_SESSION_URL = "invalid-session-url"; private static final String ATT_INVALID_SESSION_URL = "invalid-session-url";
private static final String ATT_SESSION_AUTH_STRATEGY_REF = "session-authentication-strategy-ref"; private static final String ATT_SESSION_AUTH_STRATEGY_REF = "session-authentication-strategy-ref";
@ -247,6 +255,7 @@ class HttpConfigurationBuilder {
String sessionAuthStratRef = null; String sessionAuthStratRef = null;
String errorUrl = null; String errorUrl = null;
boolean sessionControlEnabled = false;
if (sessionMgmtElt != null) { if (sessionMgmtElt != null) {
if (sessionPolicy == SessionCreationPolicy.STATELESS) { if (sessionPolicy == SessionCreationPolicy.STATELESS) {
pc.getReaderContext().error(Elements.SESSION_MANAGEMENT + " cannot be used" + pc.getReaderContext().error(Elements.SESSION_MANAGEMENT + " cannot be used" +
@ -258,8 +267,9 @@ class HttpConfigurationBuilder {
sessionAuthStratRef = sessionMgmtElt.getAttribute(ATT_SESSION_AUTH_STRATEGY_REF); sessionAuthStratRef = sessionMgmtElt.getAttribute(ATT_SESSION_AUTH_STRATEGY_REF);
errorUrl = sessionMgmtElt.getAttribute(ATT_SESSION_AUTH_ERROR_URL); errorUrl = sessionMgmtElt.getAttribute(ATT_SESSION_AUTH_ERROR_URL);
sessionCtrlElt = DomUtils.getChildElementByTagName(sessionMgmtElt, Elements.CONCURRENT_SESSIONS); sessionCtrlElt = DomUtils.getChildElementByTagName(sessionMgmtElt, Elements.CONCURRENT_SESSIONS);
sessionControlEnabled = sessionCtrlElt != null;
if (sessionCtrlElt != null) { if (sessionControlEnabled) {
if (StringUtils.hasText(sessionAuthStratRef)) { if (StringUtils.hasText(sessionAuthStratRef)) {
pc.getReaderContext().error(ATT_SESSION_AUTH_STRATEGY_REF + " attribute cannot be used" + pc.getReaderContext().error(ATT_SESSION_AUTH_STRATEGY_REF + " attribute cannot be used" +
" in combination with <" + Elements.CONCURRENT_SESSIONS + ">", pc.extractSource(sessionCtrlElt)); " in combination with <" + Elements.CONCURRENT_SESSIONS + ">", pc.extractSource(sessionCtrlElt));
@ -269,7 +279,8 @@ class HttpConfigurationBuilder {
} }
if (!StringUtils.hasText(sessionFixationAttribute)) { if (!StringUtils.hasText(sessionFixationAttribute)) {
sessionFixationAttribute = OPT_SESSION_FIXATION_MIGRATE_SESSION; Method changeSessionIdMethod = ReflectionUtils.findMethod(HttpServletRequest.class, "changeSessionId");
sessionFixationAttribute = changeSessionIdMethod == null ? OPT_SESSION_FIXATION_MIGRATE_SESSION : OPT_CHANGE_SESSION_ID;
} else if (StringUtils.hasText(sessionAuthStratRef)) { } else if (StringUtils.hasText(sessionAuthStratRef)) {
pc.getReaderContext().error(ATT_SESSION_FIXATION_PROTECTION + " attribute cannot be used" + pc.getReaderContext().error(ATT_SESSION_FIXATION_PROTECTION + " attribute cannot be used" +
" in combination with " + ATT_SESSION_AUTH_STRATEGY_REF, pc.extractSource(sessionMgmtElt)); " in combination with " + ATT_SESSION_AUTH_STRATEGY_REF, pc.extractSource(sessionMgmtElt));
@ -282,28 +293,50 @@ class HttpConfigurationBuilder {
boolean sessionFixationProtectionRequired = !sessionFixationAttribute.equals(OPT_SESSION_FIXATION_NO_PROTECTION); boolean sessionFixationProtectionRequired = !sessionFixationAttribute.equals(OPT_SESSION_FIXATION_NO_PROTECTION);
BeanDefinitionBuilder sessionStrategy; ManagedList<BeanMetadataElement> delegateSessionStrategies = new ManagedList<BeanMetadataElement>();
BeanDefinitionBuilder concurrentSessionStrategy;
BeanDefinitionBuilder sessionFixationStrategy = null;
BeanDefinitionBuilder registerSessionStrategy;
if (sessionCtrlElt != null) { if (sessionControlEnabled) {
assert sessionRegistryRef != null; assert sessionRegistryRef != null;
sessionStrategy = BeanDefinitionBuilder.rootBeanDefinition(ConcurrentSessionControlStrategy.class); concurrentSessionStrategy = BeanDefinitionBuilder.rootBeanDefinition(ConcurrentSessionControlAuthenticationStrategy.class);
sessionStrategy.addConstructorArgValue(sessionRegistryRef); concurrentSessionStrategy.addConstructorArgValue(sessionRegistryRef);
String maxSessions = sessionCtrlElt.getAttribute("max-sessions"); String maxSessions = sessionCtrlElt.getAttribute("max-sessions");
if (StringUtils.hasText(maxSessions)) { if (StringUtils.hasText(maxSessions)) {
sessionStrategy.addPropertyValue("maximumSessions", maxSessions); concurrentSessionStrategy.addPropertyValue("maximumSessions", maxSessions);
} }
String exceptionIfMaximumExceeded = sessionCtrlElt.getAttribute("error-if-maximum-exceeded"); String exceptionIfMaximumExceeded = sessionCtrlElt.getAttribute("error-if-maximum-exceeded");
if (StringUtils.hasText(exceptionIfMaximumExceeded)) { if (StringUtils.hasText(exceptionIfMaximumExceeded)) {
sessionStrategy.addPropertyValue("exceptionIfMaximumExceeded", exceptionIfMaximumExceeded); concurrentSessionStrategy.addPropertyValue("exceptionIfMaximumExceeded", exceptionIfMaximumExceeded);
} }
} else if (sessionFixationProtectionRequired || StringUtils.hasText(invalidSessionUrl) delegateSessionStrategies.add(concurrentSessionStrategy.getBeanDefinition());
|| StringUtils.hasText(sessionAuthStratRef)) { }
sessionStrategy = BeanDefinitionBuilder.rootBeanDefinition(SessionFixationProtectionStrategy.class); boolean useChangeSessionId = OPT_CHANGE_SESSION_ID.equals(sessionFixationAttribute);
} else { if (sessionFixationProtectionRequired || StringUtils.hasText(invalidSessionUrl)) {
if(useChangeSessionId) {
sessionFixationStrategy = BeanDefinitionBuilder.rootBeanDefinition(ChangeSessionIdAuthenticationStrategy.class);
} else {
sessionFixationStrategy = BeanDefinitionBuilder.rootBeanDefinition(SessionFixationProtectionStrategy.class);
}
delegateSessionStrategies.add(sessionFixationStrategy.getBeanDefinition());
}
if(StringUtils.hasText(sessionAuthStratRef)) {
delegateSessionStrategies.add(new RuntimeBeanReference(sessionAuthStratRef));
}
if(sessionControlEnabled) {
registerSessionStrategy = BeanDefinitionBuilder.rootBeanDefinition(RegisterSessionAuthenticationStrategy.class);
registerSessionStrategy.addConstructorArgValue(sessionRegistryRef);
delegateSessionStrategies.add(registerSessionStrategy.getBeanDefinition());
}
if(delegateSessionStrategies.isEmpty()) {
sfpf = null; sfpf = null;
return; return;
} }
@ -316,15 +349,21 @@ class HttpConfigurationBuilder {
sessionMgmtFilter.addPropertyValue("authenticationFailureHandler", failureHandler); sessionMgmtFilter.addPropertyValue("authenticationFailureHandler", failureHandler);
sessionMgmtFilter.addConstructorArgValue(contextRepoRef); sessionMgmtFilter.addConstructorArgValue(contextRepoRef);
if (!StringUtils.hasText(sessionAuthStratRef)) { if (!StringUtils.hasText(sessionAuthStratRef) && sessionFixationStrategy != null && !useChangeSessionId ) {
BeanDefinition strategyBean = sessionStrategy.getBeanDefinition();
if (sessionFixationProtectionRequired) { if (sessionFixationProtectionRequired) {
sessionStrategy.addPropertyValue("migrateSessionAttributes", sessionFixationStrategy.addPropertyValue("migrateSessionAttributes",
Boolean.valueOf(sessionFixationAttribute.equals(OPT_SESSION_FIXATION_MIGRATE_SESSION))); Boolean.valueOf(sessionFixationAttribute.equals(OPT_SESSION_FIXATION_MIGRATE_SESSION)));
} }
}
if(!delegateSessionStrategies.isEmpty()) {
BeanDefinitionBuilder sessionStrategy = BeanDefinitionBuilder.rootBeanDefinition(CompositeSessionAuthenticationStrategy.class);
BeanDefinition strategyBean = sessionStrategy.getBeanDefinition();
sessionStrategy.addConstructorArgValue(delegateSessionStrategies);
sessionAuthStratRef = pc.getReaderContext().generateBeanName(strategyBean); sessionAuthStratRef = pc.getReaderContext().generateBeanName(strategyBean);
pc.registerBeanComponent(new BeanComponentDefinition(strategyBean, sessionAuthStratRef)); pc.registerBeanComponent(new BeanComponentDefinition(strategyBean, sessionAuthStratRef));
} }
if (StringUtils.hasText(invalidSessionUrl)) { if (StringUtils.hasText(invalidSessionUrl)) {

View File

@ -58,12 +58,13 @@ class NamespaceSessionManagementTests extends BaseSpringSpec {
CustomSessionManagementConfig.SR = Mock(SessionRegistry) CustomSessionManagementConfig.SR = Mock(SessionRegistry)
when: when:
loadConfig(CustomSessionManagementConfig) loadConfig(CustomSessionManagementConfig)
def concurrentStrategy = findFilter(SessionManagementFilter).sessionAuthenticationStrategy.delegateStrategies[0]
then: then:
findFilter(SessionManagementFilter).invalidSessionStrategy.destinationUrl == "/invalid-session" findFilter(SessionManagementFilter).invalidSessionStrategy.destinationUrl == "/invalid-session"
findFilter(SessionManagementFilter).failureHandler.defaultFailureUrl == "/session-auth-error" findFilter(SessionManagementFilter).failureHandler.defaultFailureUrl == "/session-auth-error"
findFilter(SessionManagementFilter).sessionAuthenticationStrategy.maximumSessions == 1 concurrentStrategy.maximumSessions == 1
findFilter(SessionManagementFilter).sessionAuthenticationStrategy.exceptionIfMaximumExceeded concurrentStrategy.exceptionIfMaximumExceeded
findFilter(SessionManagementFilter).sessionAuthenticationStrategy.sessionRegistry == CustomSessionManagementConfig.SR concurrentStrategy.sessionRegistry == CustomSessionManagementConfig.SR
findFilter(ConcurrentSessionFilter).expiredUrl == "/expired-session" findFilter(ConcurrentSessionFilter).expiredUrl == "/expired-session"
} }
@ -154,7 +155,8 @@ class NamespaceSessionManagementTests extends BaseSpringSpec {
protected void configure(HttpSecurity http) throws Exception { protected void configure(HttpSecurity http) throws Exception {
http http
.sessionManagement() .sessionManagement()
.sessionAuthenticationStrategy(new SessionFixationProtectionStrategy(migrateSessionAttributes : false)) .sessionFixation()
.newSession()
} }
} }
} }

View File

@ -15,16 +15,24 @@
*/ */
package org.springframework.security.config.annotation.web.configurers package org.springframework.security.config.annotation.web.configurers
import javax.servlet.http.HttpServletResponse
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import org.springframework.mock.web.MockFilterChain
import org.springframework.mock.web.MockHttpServletRequest
import org.springframework.mock.web.MockHttpServletResponse
import org.springframework.security.config.annotation.AnyObjectPostProcessor import org.springframework.security.config.annotation.AnyObjectPostProcessor
import org.springframework.security.config.annotation.BaseSpringSpec import org.springframework.security.config.annotation.BaseSpringSpec
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.web.access.ExceptionTranslationFilter import org.springframework.security.web.access.ExceptionTranslationFilter
import org.springframework.security.web.context.NullSecurityContextRepository; import org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy
import org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy
import org.springframework.security.web.context.NullSecurityContextRepository
import org.springframework.security.web.context.SecurityContextPersistenceFilter import org.springframework.security.web.context.SecurityContextPersistenceFilter
import org.springframework.security.web.context.SecurityContextRepository import org.springframework.security.web.context.SecurityContextRepository
import org.springframework.security.web.savedrequest.RequestCache import org.springframework.security.web.savedrequest.RequestCache
@ -110,6 +118,82 @@ class SessionManagementConfigurerTests extends BaseSpringSpec {
} }
def 'SEC-2137: disable session fixation and enable concurrency control'() {
setup: "context where session fixation is disabled and concurrency control is enabled"
loadConfig(DisableSessionFixationEnableConcurrencyControlConfig)
String originalSessionId = request.session.id
String credentials = "user:password"
request.addHeader("Authorization", "Basic " + credentials.bytes.encodeBase64())
when: "authenticate"
springSecurityFilterChain.doFilter(request, response, new MockFilterChain())
then: "session invalidate is not called"
request.session.id == originalSessionId
}
@EnableWebSecurity
@Configuration
static class DisableSessionFixationEnableConcurrencyControlConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) {
http
.httpBasic()
.and()
.sessionManagement()
.sessionFixation().none()
.maximumSessions(1)
}
@Override
public void registerAuthentication(AuthenticationManagerBuilder auth) {
auth
.inMemoryAuthentication()
.withUser("user").password("password").roles("USER")
}
}
def 'session fixation and enable concurrency control'() {
setup: "context where session fixation is disabled and concurrency control is enabled"
loadConfig(ConcurrencyControlConfig)
when: "authenticate successfully"
request.servletPath = "/login"
request.method = "POST"
request.setParameter("username", "user");
request.setParameter("password","password")
springSecurityFilterChain.doFilter(request, response, chain)
then: "authentication is sucessful"
response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
response.redirectedUrl == "/"
when: "authenticate with the same user"
super.setup()
request.servletPath = "/login"
request.method = "POST"
request.setParameter("username", "user");
request.setParameter("password","password")
springSecurityFilterChain.doFilter(request, response, chain)
then:
response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
response.redirectedUrl == '/login?error'
}
@EnableWebSecurity
@Configuration
static class ConcurrencyControlConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) {
http
.formLogin()
.and()
.sessionManagement()
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
}
@Override
public void registerAuthentication(AuthenticationManagerBuilder auth) {
auth
.inMemoryAuthentication()
.withUser("user").password("password").roles("USER")
}
}
def "sessionManagement ObjectPostProcessor"() { def "sessionManagement ObjectPostProcessor"() {
setup: setup:
AnyObjectPostProcessor opp = Mock() AnyObjectPostProcessor opp = Mock()
@ -122,9 +206,15 @@ class SessionManagementConfigurerTests extends BaseSpringSpec {
.and() .and()
.build() .build()
then: "SessionManagementFilter is registered with LifecycleManager" then: "SessionManagementFilter is registered with ObjectPostProcessor"
1 * opp.postProcess(_ as SessionManagementFilter) >> {SessionManagementFilter o -> o} 1 * opp.postProcess(_ as SessionManagementFilter) >> {SessionManagementFilter o -> o}
and: "ConcurrentSessionFilter is registered with LifecycleManager" and: "ConcurrentSessionFilter is registered with ObjectPostProcessor"
1 * opp.postProcess(_ as ConcurrentSessionFilter) >> {ConcurrentSessionFilter o -> o} 1 * opp.postProcess(_ as ConcurrentSessionFilter) >> {ConcurrentSessionFilter o -> o}
and: "ConcurrentSessionControlAuthenticationStrategy is registered with ObjectPostProcessor"
1 * opp.postProcess(_ as ConcurrentSessionControlAuthenticationStrategy) >> {ConcurrentSessionControlAuthenticationStrategy o -> o}
and: "CompositeSessionAuthenticationStrategy is registered with ObjectPostProcessor"
1 * opp.postProcess(_ as CompositeSessionAuthenticationStrategy) >> {CompositeSessionAuthenticationStrategy o -> o}
and: "RegisterSessionAuthenticationStrategy is registered with ObjectPostProcessor"
1 * opp.postProcess(_ as RegisterSessionAuthenticationStrategy) >> {RegisterSessionAuthenticationStrategy o -> o}
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2012 the original author or authors. * Copyright 2002-2013 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -15,28 +15,40 @@
*/ */
package org.springframework.security.config.http 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.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.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.Authentication
import org.springframework.security.core.authority.AuthorityUtils
import org.springframework.security.core.context.SecurityContext import org.springframework.security.core.context.SecurityContext
import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.core.context.SecurityContextHolder
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.core.userdetails.User
import org.springframework.security.util.FieldUtils import org.springframework.security.util.FieldUtils
import org.springframework.security.web.FilterChainProxy
import org.springframework.security.web.authentication.RememberMeServices import org.springframework.security.web.authentication.RememberMeServices
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
import org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler import org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler
import org.springframework.security.web.authentication.logout.LogoutFilter import org.springframework.security.web.authentication.logout.LogoutFilter
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler
import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter
import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy 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.NullSecurityContextRepository
import org.springframework.security.web.context.SaveContextOnUpdateOrErrorResponseWrapper import org.springframework.security.web.context.SaveContextOnUpdateOrErrorResponseWrapper
import org.springframework.security.web.context.SecurityContextPersistenceFilter import org.springframework.security.web.context.SecurityContextPersistenceFilter
import org.springframework.security.web.savedrequest.RequestCacheAwareFilter import org.springframework.security.web.savedrequest.RequestCacheAwareFilter
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 static org.junit.Assert.assertSame
/** /**
* Tests session-related functionality for the &lt;http&gt; namespace element and &lt;session-management&gt; * Tests session-related functionality for the &lt;http&gt; namespace element and &lt;session-management&gt;
@ -93,6 +105,46 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests {
filter.repo.allowSessionCreation filter.repo.allowSessionCreation
} }
def 'SEC-1208: Session is not created when rejecting user due to max sessions exceeded'() {
setup:
httpCreateSession('never') {
'session-management'() {
'concurrency-control'('max-sessions':1,'error-if-maximum-exceeded':'true')
}
}
createAppContext()
SessionRegistry registry = appContext.getBean(SessionRegistry)
registry.registerNewSession("1", new User("user","password",AuthorityUtils.createAuthorityList("ROLE_USER")))
MockHttpServletRequest request = new MockHttpServletRequest()
MockHttpServletResponse response = new MockHttpServletResponse()
String credentials = "user:password"
request.addHeader("Authorization", "Basic " + credentials.bytes.encodeBase64())
when: "exceed max authentication attempts"
appContext.getBean(FilterChainProxy).doFilter(request, response, new MockFilterChain())
then: "no new session is created"
request.getSession(false) == null
response.status == HttpServletResponse.SC_UNAUTHORIZED
}
def 'SEC-2137: disable session fixation and enable concurrency control'() {
setup: "context where session fixation is disabled and concurrency control is enabled"
httpAutoConfig {
'session-management'('session-fixation-protection':'none') {
'concurrency-control'('max-sessions':'1','error-if-maximum-exceeded':'true')
}
}
createAppContext()
MockHttpServletRequest request = new MockHttpServletRequest()
MockHttpServletResponse response = new MockHttpServletResponse()
String originalSessionId = request.session.id
String credentials = "user:password"
request.addHeader("Authorization", "Basic " + credentials.bytes.encodeBase64())
when: "authenticate"
appContext.getBean(FilterChainProxy).doFilter(request, response, new MockFilterChain())
then: "session invalidate is not called"
request.session.id == originalSessionId
}
def httpCreateSession(String create, Closure c) { def httpCreateSession(String create, Closure c) {
xml.http(['auto-config': 'true', 'create-session': create], c) xml.http(['auto-config': 'true', 'create-session': create], c)
} }
@ -219,15 +271,28 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests {
} }
def externalSessionStrategyIsSupported() { def externalSessionStrategyIsSupported() {
when: setup:
httpAutoConfig { httpAutoConfig {
'session-management'('session-authentication-strategy-ref':'ss') 'session-management'('session-authentication-strategy-ref':'ss')
} }
bean('ss', SessionFixationProtectionStrategy.class.name) xml.'b:bean'(id: 'ss', 'class': Mockito.class.name, 'factory-method':'mock') {
createAppContext(); 'b:constructor-arg'(value : SessionAuthenticationStrategy.class.name)
}
createAppContext()
then: MockHttpServletRequest request = new MockHttpServletRequest();
notThrown(Exception.class) request.getSession();
request.setRequestURI("/j_spring_security_check");
request.setMethod("POST");
request.setParameter("j_username", "user");
request.setParameter("j_password", "password");
SessionAuthenticationStrategy sessionAuthStrategy = appContext.getBean('ss',SessionAuthenticationStrategy)
FilterChainProxy springSecurityFilterChain = appContext.getBean(FilterChainProxy)
when:
springSecurityFilterChain.doFilter(request,new MockHttpServletResponse(), new MockFilterChain())
then: "CustomSessionAuthenticationStrategy has seen the request (although REQUEST is a wrapped request)"
verify(sessionAuthStrategy).onAuthentication(any(Authentication), any(HttpServletRequest), any(HttpServletResponse))
} }
def externalSessionRegistryBeanIsConfiguredCorrectly() { def externalSessionRegistryBeanIsConfiguredCorrectly() {
@ -247,10 +312,8 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests {
Object sessionRegistry = appContext.getBean("sr"); Object sessionRegistry = appContext.getBean("sr");
Object sessionRegistryFromConcurrencyFilter = FieldUtils.getFieldValue( Object sessionRegistryFromConcurrencyFilter = FieldUtils.getFieldValue(
getFilter(ConcurrentSessionFilter.class), "sessionRegistry"); getFilter(ConcurrentSessionFilter.class), "sessionRegistry");
Object sessionRegistryFromFormLoginFilter = FieldUtils.getFieldValue( Object sessionRegistryFromFormLoginFilter = FieldUtils.getFieldValue(getFilter(UsernamePasswordAuthenticationFilter),"sessionStrategy").delegateStrategies[0].sessionRegistry
getFilter(UsernamePasswordAuthenticationFilter.class),"sessionStrategy.sessionRegistry"); Object sessionRegistryFromMgmtFilter = FieldUtils.getFieldValue(getFilter(SessionManagementFilter),"sessionAuthenticationStrategy").delegateStrategies[0].sessionRegistry
Object sessionRegistryFromMgmtFilter = FieldUtils.getFieldValue(
getFilter(SessionManagementFilter.class),"sessionAuthenticationStrategy.sessionRegistry");
assertSame(sessionRegistry, sessionRegistryFromConcurrencyFilter); assertSame(sessionRegistry, sessionRegistryFromConcurrencyFilter);
assertSame(sessionRegistry, sessionRegistryFromMgmtFilter); assertSame(sessionRegistry, sessionRegistryFromMgmtFilter);
@ -297,7 +360,7 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests {
createAppContext() createAppContext()
expect: expect:
!(getFilters("/someurl")[8] instanceof SessionManagementFilter) !(getFilters("/someurl").find { it instanceof SessionManagementFilter})
} }
def disablingSessionProtectionRetainsSessionManagementFilterInvalidSessionUrlSet() { def disablingSessionProtectionRetainsSessionManagementFilterInvalidSessionUrlSet() {

View File

@ -1208,7 +1208,7 @@
<para> Adds support for concurrent session control, allowing limits to be placed on the <para> Adds support for concurrent session control, allowing limits to be placed on the
number of active sessions a user can have. A number of active sessions a user can have. A
<classname>ConcurrentSessionFilter</classname> will be created, and a <classname>ConcurrentSessionFilter</classname> will be created, and a
<classname>ConcurrentSessionControlStrategy</classname> will be used with the <classname>ConcurrentSessionControlAuthenticationStrategy</classname> will be used with the
<classname>SessionManagementFilter</classname>. If a <literal>form-login</literal> <classname>SessionManagementFilter</classname>. If a <literal>form-login</literal>
element has been declared, the strategy object will also be injected into the element has been declared, the strategy object will also be injected into the
created authentication filter. An instance of created authentication filter. An instance of
@ -1242,7 +1242,7 @@
<section xml:id="nsa-concurrency-control-max-sessions"> <section xml:id="nsa-concurrency-control-max-sessions">
<title><literal>max-sessions</literal></title> <title><literal>max-sessions</literal></title>
<para>Maps to the <literal>maximumSessions</literal> property of <para>Maps to the <literal>maximumSessions</literal> property of
<classname>ConcurrentSessionControlStrategy</classname>.</para> <classname>ConcurrentSessionControlAuthenticationStrategy</classname>.</para>
</section> </section>
<section xml:id="nsa-concurrency-control-session-registry-alias"> <section xml:id="nsa-concurrency-control-session-registry-alias">
<title><literal>session-registry-alias</literal></title> <title><literal>session-registry-alias</literal></title>

View File

@ -80,7 +80,7 @@
though. </para> though. </para>
<para>The implementation uses a specialized version of <para>The implementation uses a specialized version of
<interfacename>SessionAuthenticationStrategy</interfacename>, called <interfacename>SessionAuthenticationStrategy</interfacename>, called
<classname>ConcurrentSessionControlStrategy</classname>. <note> <classname>ConcurrentSessionControlAuthenticationStrategy</classname>. <note>
<para>Previously the concurrent authentication check was made by the <para>Previously the concurrent authentication check was made by the
<classname>ProviderManager</classname>, which could be injected with a <classname>ProviderManager</classname>, which could be injected with a
<literal>ConcurrentSessionController</literal>. The latter would check if the user <literal>ConcurrentSessionController</literal>. The latter would check if the user
@ -126,10 +126,21 @@
<beans:property name="authenticationManager" ref="authenticationManager" /> <beans:property name="authenticationManager" ref="authenticationManager" />
</beans:bean> </beans:bean>
<beans:bean id="sas" class= <beans:bean id="sas" class="org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy">
"org.springframework.security.web.authentication.session.ConcurrentSessionControlStrategy"> <beans:constructor-arg>
<beans:constructor-arg name="sessionRegistry" ref="sessionRegistry" /> <beans:list>
<beans:property name="maximumSessions" value="1" /> <beans:bean class="org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy">
<beans:constructor-arg ref="sessionRegistry"/>
<beans:property name="maximumSessions" value="1" />
<beans:property name="exceptionIfMaximumExceeded" value="true" />
</beans:bean>
<beans:bean class="org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy">
</beans:bean>
<beans:bean class="org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy">
<beans:constructor-arg ref="sessionRegistry"/>
</beans:bean>
</beans:list>
</beans:constructor-arg>
</beans:bean> </beans:bean>
<beans:bean id="sessionRegistry" <beans:bean id="sessionRegistry"

View File

@ -32,10 +32,21 @@
<beans:property name="expiredUrl" value="/session-expired.htm" /> <beans:property name="expiredUrl" value="/session-expired.htm" />
</beans:bean> </beans:bean>
<beans:bean id="sas" class="org.springframework.security.web.authentication.session.ConcurrentSessionControlStrategy"> <beans:bean id="sas" class="org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy">
<beans:constructor-arg name="sessionRegistry" ref="sessionRegistry" /> <beans:constructor-arg>
<beans:property name="maximumSessions" value="1" /> <beans:list>
<beans:property name="exceptionIfMaximumExceeded" value="true" /> <beans:bean class="org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy">
<beans:constructor-arg ref="sessionRegistry"/>
<beans:property name="maximumSessions" value="1" />
<beans:property name="exceptionIfMaximumExceeded" value="true" />
</beans:bean>
<beans:bean class="org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy">
</beans:bean>
<beans:bean class="org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy">
<beans:constructor-arg ref="sessionRegistry"/>
</beans:bean>
</beans:list>
</beans:constructor-arg>
</beans:bean> </beans:bean>
<beans:bean id="sessionRegistry" class="org.springframework.security.core.session.SessionRegistryImpl" /> <beans:bean id="sessionRegistry" class="org.springframework.security.core.session.SessionRegistryImpl" />

View File

@ -0,0 +1,91 @@
/*
R * Copyright 2002-2013 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.authentication.session;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
/**
* A {@link SessionAuthenticationStrategy} that accepts multiple
* {@link SessionAuthenticationStrategy} implementations to delegate to. Each
* {@link SessionAuthenticationStrategy} is invoked in turn. The invocations are
* short circuited if any exception, (i.e. SessionAuthenticationException) is
* thrown.
*
* <p>
* Typical usage would include having the following delegates (in this order)
* </p>
*
* <ul>
* <li> {@link ConcurrentSessionControlAuthenticationStrategy} - verifies that a
* user is allowed to authenticate (i.e. they have not already logged into the
* application.</li>
* <li> {@link SessionFixationProtectionStrategy} - If session fixation is
* desired, {@link SessionFixationProtectionStrategy} should be after
* {@link ConcurrentSessionControlAuthenticationStrategy} to prevent unnecessary
* {@link HttpSession} creation if the
* {@link ConcurrentSessionControlAuthenticationStrategy} rejects
* authentication.</li>
* <li> {@link RegisterSessionAuthenticationStrategy} - It is important this is
* after {@link SessionFixationProtectionStrategy} so that the correct session
* is registered.</li>
* </ul>
*
* @author Rob Winch
* @since 3.2
*/
public class CompositeSessionAuthenticationStrategy implements SessionAuthenticationStrategy {
private final Log logger = LogFactory.getLog(getClass());
private final List<SessionAuthenticationStrategy> delegateStrategies;
public CompositeSessionAuthenticationStrategy(List<SessionAuthenticationStrategy> delegateStrategies) {
Assert.notEmpty(delegateStrategies, "delegateStrategies cannot be null or empty");
for(SessionAuthenticationStrategy strategy : delegateStrategies) {
if(strategy == null) {
throw new IllegalArgumentException("delegateStrategies cannot contain null entires. Got " + delegateStrategies);
}
}
this.delegateStrategies = new ArrayList<SessionAuthenticationStrategy>(delegateStrategies);
}
/* (non-Javadoc)
* @see org.springframework.security.web.authentication.session.SessionAuthenticationStrategy#onAuthentication(org.springframework.security.core.Authentication, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
*/
public void onAuthentication(Authentication authentication,
HttpServletRequest request, HttpServletResponse response)
throws SessionAuthenticationException {
for(SessionAuthenticationStrategy delegate : delegateStrategies) {
if(logger.isDebugEnabled()) {
logger.debug("Delegating to " + delegate);
}
delegate.onAuthentication(authentication, request, response);
}
}
@Override
public String toString() {
return getClass().getName() + " [delegateStrategies = " + delegateStrategies + "]";
}
}

View File

@ -0,0 +1,183 @@
package org.springframework.security.web.authentication.session;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.session.ConcurrentSessionFilter;
import org.springframework.security.web.session.SessionManagementFilter;
import org.springframework.util.Assert;
/**
* Strategy which handles concurrent session-control.
*
* <p>
* When invoked following an authentication, it will check whether the user in
* question should be allowed to proceed, by comparing the number of sessions
* they already have active with the configured <tt>maximumSessions</tt> value.
* The {@link SessionRegistry} is used as the source of data on authenticated
* users and session data.
* </p>
* <p>
* If a user has reached the maximum number of permitted sessions, the behaviour
* depends on the <tt>exceptionIfMaxExceeded</tt> property. The default
* behaviour is to expired the least recently used session, which will be
* invalidated by the {@link ConcurrentSessionFilter} if accessed again. If
* <tt>exceptionIfMaxExceeded</tt> is set to <tt>true</tt>, however, the user
* will be prevented from starting a new authenticated session.
* </p>
* <p>
* This strategy can be injected into both the {@link SessionManagementFilter}
* and instances of {@link AbstractAuthenticationProcessingFilter} (typically
* {@link UsernamePasswordAuthenticationFilter}), but is typically combined with
* {@link RegisterSessionAuthenticationStrategy} using
* {@link CompositeSessionAuthenticationStrategy}.
* </p>
*
* @see CompositeSessionAuthenticationStrategy
*
* @author Luke Taylor
* @author Rob Winch
* @since 3.2
*/
public class ConcurrentSessionControlAuthenticationStrategy implements MessageSourceAware, SessionAuthenticationStrategy {
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private final SessionRegistry sessionRegistry;
private boolean exceptionIfMaximumExceeded = false;
private int maximumSessions = 1;
/**
* @param sessionRegistry the session registry which should be updated when the authenticated session is changed.
*/
public ConcurrentSessionControlAuthenticationStrategy(SessionRegistry sessionRegistry) {
Assert.notNull(sessionRegistry, "The sessionRegistry cannot be null");
this.sessionRegistry = sessionRegistry;
}
/**
* In addition to the steps from the superclass, the sessionRegistry will be updated with the new session information.
*/
public void onAuthentication(Authentication authentication, HttpServletRequest request,
HttpServletResponse response) {
final List<SessionInformation> sessions = sessionRegistry.getAllSessions(authentication.getPrincipal(), false);
int sessionCount = sessions.size();
int allowedSessions = getMaximumSessionsForThisUser(authentication);
if (sessionCount < allowedSessions) {
// They haven't got too many login sessions running at present
return;
}
if (allowedSessions == -1) {
// We permit unlimited logins
return;
}
if (sessionCount == allowedSessions) {
HttpSession session = request.getSession(false);
if (session != null) {
// Only permit it though if this request is associated with one of the already registered sessions
for (SessionInformation si : sessions) {
if (si.getSessionId().equals(session.getId())) {
return;
}
}
}
// If the session is null, a new one will be created by the parent class, exceeding the allowed number
}
allowableSessionsExceeded(sessions, allowedSessions, sessionRegistry);
}
/**
* Method intended for use by subclasses to override the maximum number of sessions that are permitted for
* a particular authentication. The default implementation simply returns the <code>maximumSessions</code> value
* for the bean.
*
* @param authentication to determine the maximum sessions for
*
* @return either -1 meaning unlimited, or a positive integer to limit (never zero)
*/
protected int getMaximumSessionsForThisUser(Authentication authentication) {
return maximumSessions;
}
/**
* Allows subclasses to customise behaviour when too many sessions are detected.
*
* @param sessions either <code>null</code> or all unexpired sessions associated with the principal
* @param allowableSessions the number of concurrent sessions the user is allowed to have
* @param registry an instance of the <code>SessionRegistry</code> for subclass use
*
*/
protected void allowableSessionsExceeded(List<SessionInformation> sessions, int allowableSessions,
SessionRegistry registry) throws SessionAuthenticationException {
if (exceptionIfMaximumExceeded || (sessions == null)) {
throw new SessionAuthenticationException(messages.getMessage("ConcurrentSessionControlAuthenticationStrategy.exceededAllowed",
new Object[] {Integer.valueOf(allowableSessions)},
"Maximum sessions of {0} for this principal exceeded"));
}
// Determine least recently used session, and mark it for invalidation
SessionInformation leastRecentlyUsed = null;
for (SessionInformation session : sessions) {
if ((leastRecentlyUsed == null)
|| session.getLastRequest().before(leastRecentlyUsed.getLastRequest())) {
leastRecentlyUsed = session;
}
}
leastRecentlyUsed.expireNow();
}
/**
* Sets the <tt>exceptionIfMaximumExceeded</tt> property, which determines
* whether the user should be prevented from opening more sessions than
* allowed. If set to <tt>true</tt>, a
* <tt>SessionAuthenticationException</tt> will be raised which means the
* user authenticating will be prevented from authenticating. if set to
* <tt>false</tt>, the user that has already authenticated will be forcibly
* logged out.
*
* @param exceptionIfMaximumExceeded
* defaults to <tt>false</tt>.
*/
public void setExceptionIfMaximumExceeded(boolean exceptionIfMaximumExceeded) {
this.exceptionIfMaximumExceeded = exceptionIfMaximumExceeded;
}
/**
* Sets the <tt>maxSessions</tt> property. The default value is 1. Use -1 for unlimited sessions.
*
* @param maximumSessions the maximimum number of permitted sessions a user can have open simultaneously.
*/
public void setMaximumSessions(int maximumSessions) {
Assert.isTrue(maximumSessions != 0,
"MaximumLogins must be either -1 to allow unlimited logins, or a positive integer to specify a maximum");
this.maximumSessions = maximumSessions;
}
/**
* Sets the {@link MessageSource} used for reporting errors back to the user
* when the user has exceeded the maximum number of authentications.
*/
public void setMessageSource(MessageSource messageSource) {
Assert.notNull(messageSource, "messageSource cannot be null");
this.messages = new MessageSourceAccessor(messageSource);
}
}

View File

@ -37,7 +37,9 @@ import org.springframework.util.Assert;
* *
* @author Luke Taylor * @author Luke Taylor
* @since 3.0 * @since 3.0
* @deprecated Use {@link ConcurrentSessionControlAuthenticationStrategy} instead
*/ */
@Deprecated
public class ConcurrentSessionControlStrategy extends SessionFixationProtectionStrategy public class ConcurrentSessionControlStrategy extends SessionFixationProtectionStrategy
implements MessageSourceAware { implements MessageSourceAware {
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

View File

@ -0,0 +1,51 @@
package org.springframework.security.web.authentication.session;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import org.springframework.util.Assert;
/**
* Strategy used to register a user with the {@link SessionRegistry} after
* successful {@link Authentication}.
*
* <p>
* {@link RegisterSessionAuthenticationStrategy} is typically used in
* combination with {@link CompositeSessionAuthenticationStrategy} and
* {@link ConcurrentSessionControlAuthenticationStrategy}, but can be used on
* its own if tracking of sessions is desired but no need to control
* concurrency.</P
*
* <p>
* NOTE: When using a {@link SessionRegistry} it is important that all sessions
* (including timed out sessions) are removed. This is typically done by adding
* {@link HttpSessionEventPublisher}.</p>
*
* @see CompositeSessionAuthenticationStrategy
*
* @author Luke Taylor
* @author Rob Winch
* @since 3.2
*/
public class RegisterSessionAuthenticationStrategy implements SessionAuthenticationStrategy {
private final SessionRegistry sessionRegistry;
/**
* @param sessionRegistry the session registry which should be updated when the authenticated session is changed.
*/
public RegisterSessionAuthenticationStrategy(SessionRegistry sessionRegistry) {
Assert.notNull(sessionRegistry, "The sessionRegistry cannot be null");
this.sessionRegistry = sessionRegistry;
}
/**
* In addition to the steps from the superclass, the sessionRegistry will be updated with the new session information.
*/
public void onAuthentication(Authentication authentication, HttpServletRequest request,
HttpServletResponse response) {
sessionRegistry.registerNewSession(request.getSession().getId(), authentication.getPrincipal());
}
}

View File

@ -0,0 +1,92 @@
/*
* Copyright 2002-2013 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.authentication.session;
import static junit.framework.Assert.fail;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import java.util.Arrays;
import java.util.Collections;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.security.core.Authentication;
/**
* @author Rob Winch
*
*/
@RunWith(MockitoJUnitRunner.class)
public class CompositeSessionAuthenticationStrategyTests {
@Mock
private SessionAuthenticationStrategy strategy1;
@Mock
private SessionAuthenticationStrategy strategy2;
@Mock
private Authentication authentication;
@Mock
private HttpServletRequest request;
@Mock
private HttpServletResponse response;
@Test(expected = IllegalArgumentException.class)
public void constructorNullDelegates() {
new CompositeSessionAuthenticationStrategy(null);
}
@Test(expected = IllegalArgumentException.class)
public void constructorEmptyDelegates() {
new CompositeSessionAuthenticationStrategy(Collections.<SessionAuthenticationStrategy>emptyList());
}
@Test(expected = IllegalArgumentException.class)
public void constructorDelegatesContainNull() {
new CompositeSessionAuthenticationStrategy(Collections.<SessionAuthenticationStrategy>singletonList(null));
}
@Test
public void delegatesToAll() {
CompositeSessionAuthenticationStrategy strategy = new CompositeSessionAuthenticationStrategy(Arrays.asList(strategy1,strategy2));
strategy.onAuthentication(authentication, request, response);
verify(strategy1).onAuthentication(authentication, request, response);
verify(strategy2).onAuthentication(authentication, request, response);
}
@Test
public void delegateShortCircuits() {
doThrow(new SessionAuthenticationException("oops")).when(strategy1).onAuthentication(authentication, request, response);
CompositeSessionAuthenticationStrategy strategy = new CompositeSessionAuthenticationStrategy(Arrays.asList(strategy1,strategy2));
try {
strategy.onAuthentication(authentication, request, response);
fail("Expected Exception");
} catch (SessionAuthenticationException success) {}
verify(strategy1).onAuthentication(authentication, request, response);
verify(strategy2,times(0)).onAuthentication(authentication, request, response);
}
}

View File

@ -0,0 +1,132 @@
/*
* Copyright 2002-2013 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.authentication.session;
import static org.fest.assertions.Assertions.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Mockito.when;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.mock.web.MockServletContext;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
/**
*
* @author Rob Winch
*
*/
@RunWith(MockitoJUnitRunner.class)
public class ConcurrentSessionControlAuthenticationStrategyTests {
@Mock
private SessionRegistry sessionRegistry;
private Authentication authentication;
private MockHttpServletRequest request;
private MockHttpServletResponse response;
private SessionInformation sessionInformation;
private ConcurrentSessionControlAuthenticationStrategy strategy;
@Before
public void setup() throws Exception {
authentication = new TestingAuthenticationToken("user", "password", "ROLE_USER");
request = new MockHttpServletRequest();
response = new MockHttpServletResponse();
sessionInformation = new SessionInformation(authentication.getPrincipal(), "unique", new Date(1374766134216L));
strategy = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry);
}
@Test(expected = IllegalArgumentException.class)
public void constructorNullRegistry() {
new ConcurrentSessionControlAuthenticationStrategy(null);
}
@Test
public void noRegisteredSession() {
when(sessionRegistry.getAllSessions(any(), anyBoolean())).thenReturn(Collections.<SessionInformation>emptyList());
strategy.setMaximumSessions(1);
strategy.setExceptionIfMaximumExceeded(true);
strategy.onAuthentication(authentication, request, response);
// no exception
}
@Test
public void maxSessionsSameSessionId() {
MockHttpSession session = new MockHttpSession(new MockServletContext(), sessionInformation.getSessionId());
request.setSession(session);
when(sessionRegistry.getAllSessions(any(), anyBoolean())).thenReturn(Collections.<SessionInformation>singletonList(sessionInformation));
strategy.setMaximumSessions(1);
strategy.setExceptionIfMaximumExceeded(true);
strategy.onAuthentication(authentication, request, response);
// no exception
}
@Test(expected = SessionAuthenticationException.class)
public void maxSessionsWithException() {
when(sessionRegistry.getAllSessions(any(), anyBoolean())).thenReturn(Collections.<SessionInformation>singletonList(sessionInformation));
strategy.setMaximumSessions(1);
strategy.setExceptionIfMaximumExceeded(true);
strategy.onAuthentication(authentication, request, response);
}
@Test
public void maxSessionsExpireExistingUser() {
when(sessionRegistry.getAllSessions(any(), anyBoolean())).thenReturn(Collections.<SessionInformation>singletonList(sessionInformation));
strategy.setMaximumSessions(1);
strategy.onAuthentication(authentication, request, response);
assertThat(sessionInformation.isExpired()).isTrue();
}
@Test
public void maxSessionsExpireLeastRecentExistingUser() {
SessionInformation moreRecentSessionInfo = new SessionInformation(authentication.getPrincipal(), "unique", new Date(1374766999999L));
when(sessionRegistry.getAllSessions(any(), anyBoolean())).thenReturn(Arrays.<SessionInformation>asList(moreRecentSessionInfo,sessionInformation));
strategy.setMaximumSessions(2);
strategy.onAuthentication(authentication, request, response);
assertThat(sessionInformation.isExpired()).isTrue();
}
@Test(expected = IllegalArgumentException.class)
public void setMessageSourceNull() {
strategy.setMessageSource(null);
}
}

View File

@ -0,0 +1,67 @@
/*
* Copyright 2002-2013 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.authentication.session;
import static org.mockito.Mockito.verify;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.session.SessionRegistry;
/**
* @author Rob Winch
*
*/
@RunWith(MockitoJUnitRunner.class)
public class RegisterSessionAuthenticationStrategyTests {
@Mock
private SessionRegistry registry;
private RegisterSessionAuthenticationStrategy authenticationStrategy;
private Authentication authentication;
private MockHttpServletRequest request;
private MockHttpServletResponse response;
@Before
public void setup() {
authenticationStrategy = new RegisterSessionAuthenticationStrategy(registry);
authentication = new TestingAuthenticationToken("user", "password","ROLE_USER");
request = new MockHttpServletRequest();
response = new MockHttpServletResponse();
}
@Test(expected = IllegalArgumentException.class)
public void constructorNullRegistry() {
new RegisterSessionAuthenticationStrategy(null);
}
@Test
public void onAuthenticationRegistersSession() {
authenticationStrategy.onAuthentication(authentication, request, response);
verify(registry).registerNewSession(request.getSession().getId(), authentication.getPrincipal());
}
}