diff --git a/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java b/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java index 63ccaed63d..26a19612d8 100644 --- a/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java +++ b/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java @@ -42,6 +42,7 @@ import org.springframework.security.web.access.AccessDeniedHandlerImpl; import org.springframework.security.web.access.ExceptionTranslationFilter; import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider; import org.springframework.security.web.authentication.preauth.PreAuthenticatedGrantedAuthoritiesUserDetailsService; import org.springframework.security.web.authentication.preauth.j2ee.J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource; @@ -116,6 +117,8 @@ final class AuthenticationConfigBuilder { private RootBeanDefinition preAuthEntryPoint; private BeanDefinition logoutFilter; + @SuppressWarnings("rawtypes") + private ManagedList logoutHandlers; private BeanDefinition loginPageGenerationFilter; private BeanDefinition etf; private final BeanReference requestCache; @@ -479,10 +482,23 @@ final class AuthenticationConfigBuilder { void createLogoutFilter() { Element logoutElt = DomUtils.getChildElementByTagName(httpElt, Elements.LOGOUT); if (logoutElt != null || autoConfig) { - logoutFilter = new LogoutBeanDefinitionParser(rememberMeServicesId).parse(logoutElt, pc); + LogoutBeanDefinitionParser logoutParser = new LogoutBeanDefinitionParser(rememberMeServicesId); + logoutFilter = logoutParser.parse(logoutElt, pc); + logoutHandlers = logoutParser.getLogoutHandlers(); } } + @SuppressWarnings({ "rawtypes", "unchecked" }) + ManagedList getLogoutHandlers() { + if(logoutHandlers == null && rememberMeProviderRef != null) { + logoutHandlers = new ManagedList(); + logoutHandlers.add(new RuntimeBeanReference(rememberMeServicesId)); + logoutHandlers.add(new RootBeanDefinition(SecurityContextLogoutHandler.class)); + } + + return logoutHandlers; + } + void createAnonymousFilter() { Element anonymousElt = DomUtils.getChildElementByTagName(httpElt, Elements.ANONYMOUS); diff --git a/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java b/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java index 657b15fdc2..cd959528e8 100644 --- a/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java +++ b/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java @@ -147,6 +147,13 @@ class HttpConfigurationBuilder { createFilterSecurityInterceptor(authenticationManager); } + @SuppressWarnings("rawtypes") + void setLogoutHandlers(ManagedList logoutHandlers) { + if(logoutHandlers != null && concurrentSessionFilter != null) { + concurrentSessionFilter.getPropertyValues().add("logoutHandlers", logoutHandlers); + } + } + // Needed to account for placeholders static String createPath(String path, boolean lowerCase) { return lowerCase ? path.toLowerCase() : path; diff --git a/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java index 93e060cfca..46eabadd3e 100644 --- a/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java @@ -1,3 +1,18 @@ +/* + * Copyright 2002-2012 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.config.http; import org.apache.commons.logging.Log; @@ -123,6 +138,8 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser { httpBldr.getSessionCreationPolicy(), httpBldr.getRequestCache(), authenticationManager, httpBldr.getSessionStrategy(), portMapper, portResolver); + httpBldr.setLogoutHandlers(authBldr.getLogoutHandlers()); + authenticationProviders.addAll(authBldr.getProviders()); List unorderedFilterChain = new ArrayList(); diff --git a/config/src/main/java/org/springframework/security/config/http/LogoutBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/LogoutBeanDefinitionParser.java index 6f3d6aff0d..ace6458eef 100644 --- a/config/src/main/java/org/springframework/security/config/http/LogoutBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/LogoutBeanDefinitionParser.java @@ -1,3 +1,18 @@ +/* + * Copyright 2002-2012 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.config.http; import org.springframework.beans.factory.config.BeanDefinition; @@ -29,6 +44,7 @@ class LogoutBeanDefinitionParser implements BeanDefinitionParser { static final String ATT_DELETE_COOKIES = "delete-cookies"; final String rememberMeServices; + private ManagedList logoutHandlers = new ManagedList(); public LogoutBeanDefinitionParser(String rememberMeServices) { this.rememberMeServices = rememberMeServices; @@ -75,24 +91,27 @@ class LogoutBeanDefinitionParser implements BeanDefinitionParser { builder.addConstructorArgValue(logoutSuccessUrl); } - ManagedList handlers = new ManagedList(); BeanDefinition sclh = new RootBeanDefinition(SecurityContextLogoutHandler.class); sclh.getPropertyValues().addPropertyValue("invalidateHttpSession", !"false".equals(invalidateSession)); - handlers.add(sclh); + logoutHandlers.add(sclh); if (rememberMeServices != null) { - handlers.add(new RuntimeBeanReference(rememberMeServices)); + logoutHandlers.add(new RuntimeBeanReference(rememberMeServices)); } if (StringUtils.hasText(deleteCookies)) { BeanDefinition cookieDeleter = new RootBeanDefinition(CookieClearingLogoutHandler.class); String[] names = StringUtils.tokenizeToStringArray(deleteCookies, ","); cookieDeleter.getConstructorArgumentValues().addGenericArgumentValue(names); - handlers.add(cookieDeleter); + logoutHandlers.add(cookieDeleter); } - builder.addConstructorArgValue(handlers); + builder.addConstructorArgValue(logoutHandlers); return builder.getBeanDefinition(); } + + ManagedList getLogoutHandlers() { + return logoutHandlers; + } } diff --git a/config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy index 6336018213..16db3d69f6 100644 --- a/config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy @@ -23,7 +23,12 @@ import org.springframework.security.core.context.SecurityContext import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.core.session.SessionRegistryImpl import org.springframework.security.util.FieldUtils +import org.springframework.security.web.authentication.RememberMeServices import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter +import org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler +import org.springframework.security.web.authentication.logout.LogoutFilter +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler +import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy import org.springframework.security.web.context.NullSecurityContextRepository import org.springframework.security.web.context.SaveContextOnUpdateOrErrorResponseWrapper @@ -93,6 +98,7 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests { } def concurrentSessionSupportAddsFilterAndExpectedBeans() { + when: httpAutoConfig { 'session-management'() { 'concurrency-control'('session-registry-alias':'sr', 'expired-url': '/expired') @@ -100,13 +106,89 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests { } createAppContext(); List filters = getFilters("/someurl"); + def concurrentSessionFilter = filters.get(0) - expect: - filters.get(0) instanceof ConcurrentSessionFilter - filters.get(0).expiredUrl == '/expired' + then: + concurrentSessionFilter instanceof ConcurrentSessionFilter + concurrentSessionFilter.expiredUrl == '/expired' appContext.getBean("sr") != null getFilter(SessionManagementFilter.class) != null sessionRegistryIsValid(); + + concurrentSessionFilter.handlers.size() == 1 + def logoutHandler = concurrentSessionFilter.handlers[0] + logoutHandler instanceof SecurityContextLogoutHandler + logoutHandler.invalidateHttpSession + + } + + def 'concurrency-control adds custom logout handlers'() { + when: 'Custom logout and remember-me' + httpAutoConfig { + 'session-management'() { + 'concurrency-control'() + } + 'logout'('invalidate-session': false, 'delete-cookies': 'testCookie') + 'remember-me'() + } + createAppContext(); + + List filters = getFilters("/someurl") + ConcurrentSessionFilter concurrentSessionFilter = filters.get(0) + def logoutHandlers = concurrentSessionFilter.handlers + + then: 'ConcurrentSessionFilter contains the customized LogoutHandlers' + logoutHandlers.size() == 3 + def securityCtxlogoutHandler = logoutHandlers.find { it instanceof SecurityContextLogoutHandler } + securityCtxlogoutHandler.invalidateHttpSession == false + def cookieClearingLogoutHandler = logoutHandlers.find { it instanceof CookieClearingLogoutHandler } + cookieClearingLogoutHandler.cookiesToClear == ['testCookie'] + def remembermeLogoutHandler = logoutHandlers.find { it instanceof RememberMeServices } + remembermeLogoutHandler == getFilter(RememberMeAuthenticationFilter.class).rememberMeServices + } + + def 'concurrency-control with remember-me and no LogoutFilter contains SecurityContextLogoutHandler and RememberMeServices as LogoutHandlers'() { + when: 'RememberMe and No LogoutFilter' + xml.http(['entry-point-ref': 'entryPoint'], { + 'session-management'() { + 'concurrency-control'() + } + 'remember-me'() + }) + bean('entryPoint', 'org.springframework.security.web.authentication.Http403ForbiddenEntryPoint') + createAppContext() + + List filters = getFilters("/someurl") + ConcurrentSessionFilter concurrentSessionFilter = filters.get(0) + def logoutHandlers = concurrentSessionFilter.handlers + + then: 'SecurityContextLogoutHandler and RememberMeServices are in ConcurrentSessionFilter logoutHandlers' + !filters.find { it instanceof LogoutFilter } + logoutHandlers.size() == 2 + def securityCtxlogoutHandler = logoutHandlers.find { it instanceof SecurityContextLogoutHandler } + securityCtxlogoutHandler.invalidateHttpSession == true + logoutHandlers.find { it instanceof RememberMeServices } == getFilter(RememberMeAuthenticationFilter).rememberMeServices + } + + def 'concurrency-control with no remember-me or LogoutFilter contains SecurityContextLogoutHandler as LogoutHandlers'() { + when: 'No Logout Filter or RememberMe' + xml.http(['entry-point-ref': 'entryPoint'], { + 'session-management'() { + 'concurrency-control'() + } + }) + bean('entryPoint', 'org.springframework.security.web.authentication.Http403ForbiddenEntryPoint') + createAppContext() + + List filters = getFilters("/someurl") + ConcurrentSessionFilter concurrentSessionFilter = filters.get(0) + def logoutHandlers = concurrentSessionFilter.handlers + + then: 'Only SecurityContextLogoutHandler is found in ConcurrentSessionFilter logoutHandlers' + !filters.find { it instanceof LogoutFilter } + logoutHandlers.size() == 1 + def securityCtxlogoutHandler = logoutHandlers.find { it instanceof SecurityContextLogoutHandler } + securityCtxlogoutHandler.invalidateHttpSession == true } def 'concurrency-control handles default expired-url as null'() {