From cffd3131f0a14f5013606dbb7d2ca43dcb1d1ad2 Mon Sep 17 00:00:00 2001 From: Luke Taylor Date: Sat, 20 Oct 2007 23:17:01 +0000 Subject: [PATCH] Added building of filter chain in post-processing, support for basic authentication and automatic generation of login page, if no loginUrl supplied. --- ...sicAuthenticationBeanDefinitionParser.java | 43 +++++ .../config/FormLoginBeanDefinitionParser.java | 96 +++++++++++ .../HttpSecurityBeanDefinitionParser.java | 149 +++++++----------- .../HttpSecurityConfigPostProcessor.java | 99 ++++++++++-- .../DefaultLoginPageGeneratingFilter.java | 105 ++++++++++++ ...HttpSecurityBeanDefinitionParserTests.java | 53 ++++++- .../security/config/http-security.xml | 20 +-- 7 files changed, 445 insertions(+), 120 deletions(-) create mode 100644 core/src/main/java/org/springframework/security/config/BasicAuthenticationBeanDefinitionParser.java create mode 100644 core/src/main/java/org/springframework/security/config/FormLoginBeanDefinitionParser.java create mode 100644 core/src/main/java/org/springframework/security/ui/webapp/DefaultLoginPageGeneratingFilter.java diff --git a/core/src/main/java/org/springframework/security/config/BasicAuthenticationBeanDefinitionParser.java b/core/src/main/java/org/springframework/security/config/BasicAuthenticationBeanDefinitionParser.java new file mode 100644 index 0000000000..cbaf137476 --- /dev/null +++ b/core/src/main/java/org/springframework/security/config/BasicAuthenticationBeanDefinitionParser.java @@ -0,0 +1,43 @@ +package org.springframework.security.config; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.security.ui.basicauth.BasicProcessingFilter; +import org.springframework.security.ui.basicauth.BasicProcessingFilterEntryPoint; +import org.w3c.dom.Element; + +/** + * Creates a {@link BasicProcessingFilter} and {@link BasicProcessingFilterEntryPoint} and + * registers them in the application context. + * + * @author Luke Taylor + * @version $Id$ + */ +public class BasicAuthenticationBeanDefinitionParser implements BeanDefinitionParser { + public static final String DEFAULT_BASIC_AUTH_FILTER_ID = "_basicAuthenticationFilter"; + public static final String DEFAULT_BASIC_AUTH_ENTRY_POINT_ID = "_basicAuthenticationEntryPoint"; + + + public BeanDefinition parse(Element elt, ParserContext parserContext) { + BeanDefinitionBuilder filterBuilder = + BeanDefinitionBuilder.rootBeanDefinition(BasicProcessingFilter.class); + RootBeanDefinition entryPoint = new RootBeanDefinition(BasicProcessingFilterEntryPoint.class); + + String realm = elt.getAttribute("realm"); + + entryPoint.getPropertyValues().addPropertyValue("realmName", realm); + + filterBuilder.addPropertyValue("authenticationEntryPoint", entryPoint); + // Detect auth manager + filterBuilder.setAutowireMode(RootBeanDefinition.AUTOWIRE_BY_TYPE); + + parserContext.getRegistry().registerBeanDefinition(DEFAULT_BASIC_AUTH_FILTER_ID, + filterBuilder.getBeanDefinition()); + parserContext.getRegistry().registerBeanDefinition(DEFAULT_BASIC_AUTH_ENTRY_POINT_ID, entryPoint); + + return null; + } +} diff --git a/core/src/main/java/org/springframework/security/config/FormLoginBeanDefinitionParser.java b/core/src/main/java/org/springframework/security/config/FormLoginBeanDefinitionParser.java new file mode 100644 index 0000000000..e103a4ce7e --- /dev/null +++ b/core/src/main/java/org/springframework/security/config/FormLoginBeanDefinitionParser.java @@ -0,0 +1,96 @@ +package org.springframework.security.config; + +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.security.ui.webapp.AuthenticationProcessingFilter; +import org.springframework.security.ui.webapp.AuthenticationProcessingFilterEntryPoint; +import org.springframework.security.ui.webapp.DefaultLoginPageGeneratingFilter; +import org.springframework.util.StringUtils; +import org.w3c.dom.Element; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * @author Luke Taylor + * @version $Id$ + */ +public class FormLoginBeanDefinitionParser implements BeanDefinitionParser { + protected final Log logger = LogFactory.getLog(getClass()); + + public static final String DEFAULT_FORM_LOGIN_FILTER_ID = "_formLoginFilter"; + public static final String DEFAULT_FORM_LOGIN_ENTRY_POINT_ID = "_formLoginEntryPoint"; + + private static final String LOGIN_URL_ATTRIBUTE = "loginUrl"; + private static final String LOGIN_PAGE_ATTRIBUTE = "loginPage"; + + private static final String FORM_LOGIN_TARGET_URL_ATTRIBUTE = "defaultTargetUrl"; + private static final String DEFAULT_FORM_LOGIN_TARGET_URL = "/index"; + + private static final String FORM_LOGIN_AUTH_FAILURE_URL_ATTRIBUTE = "defaultTargetUrl"; + // TODO: Change AbstractProcessingFilter to not need a failure URL and just write a failure message + // to the response if one isn't set. + private static final String DEFAULT_FORM_LOGIN_AUTH_FAILURE_URL = "/loginError"; + + + public BeanDefinition parse(Element elt, ParserContext parserContext) { + BeanDefinition filterBean = createFilterBean(elt); + + BeanDefinitionBuilder entryPointBuilder = + BeanDefinitionBuilder.rootBeanDefinition(AuthenticationProcessingFilterEntryPoint.class); + + + String loginPage = elt.getAttribute(LOGIN_PAGE_ATTRIBUTE); + + // If no login page has been defined, add in the default page generator. + if (!StringUtils.hasText(loginPage)) { + logger.info("No login page configured in form-login element. The default internal one will be used. Use" + + "the 'loginPage' attribute to specify the URL of the login page."); + loginPage = DefaultLoginPageGeneratingFilter.DEFAULT_LOGIN_PAGE_URL; + RootBeanDefinition loginPageFilter = new RootBeanDefinition(DefaultLoginPageGeneratingFilter.class); + loginPageFilter.getConstructorArgumentValues().addGenericArgumentValue(filterBean); + parserContext.getRegistry().registerBeanDefinition("_springSecurityLoginPageFilter", loginPageFilter); + } + + entryPointBuilder.addPropertyValue("loginFormUrl", loginPage); + + parserContext.getRegistry().registerBeanDefinition(DEFAULT_FORM_LOGIN_FILTER_ID, filterBean); + parserContext.getRegistry().registerBeanDefinition(DEFAULT_FORM_LOGIN_ENTRY_POINT_ID, + entryPointBuilder.getBeanDefinition()); + + return null; + } + + private BeanDefinition createFilterBean(Element elt) { + BeanDefinitionBuilder filterBuilder = + BeanDefinitionBuilder.rootBeanDefinition(AuthenticationProcessingFilter.class); + + String loginUrl = elt.getAttribute(LOGIN_URL_ATTRIBUTE); + + if (StringUtils.hasText(loginUrl)) { + filterBuilder.addPropertyValue("filterProcessesUrl", loginUrl); + } + + String defaultTargetUrl = elt.getAttribute(FORM_LOGIN_TARGET_URL_ATTRIBUTE); + + if (!StringUtils.hasText(defaultTargetUrl)) { + defaultTargetUrl = DEFAULT_FORM_LOGIN_TARGET_URL; + } + + filterBuilder.addPropertyValue("defaultTargetUrl", defaultTargetUrl); + + String authenticationFailureUrl = elt.getAttribute(FORM_LOGIN_AUTH_FAILURE_URL_ATTRIBUTE); + + if (!StringUtils.hasText(authenticationFailureUrl)) { + authenticationFailureUrl = DEFAULT_FORM_LOGIN_AUTH_FAILURE_URL; + } + + filterBuilder.addPropertyValue("authenticationFailureUrl", authenticationFailureUrl); + // Set autowire to pick up the authentication manager. + filterBuilder.setAutowireMode(RootBeanDefinition.AUTOWIRE_BY_TYPE); + + return filterBuilder.getBeanDefinition(); + } +} diff --git a/core/src/main/java/org/springframework/security/config/HttpSecurityBeanDefinitionParser.java b/core/src/main/java/org/springframework/security/config/HttpSecurityBeanDefinitionParser.java index df1922e4cb..3c69d07403 100644 --- a/core/src/main/java/org/springframework/security/config/HttpSecurityBeanDefinitionParser.java +++ b/core/src/main/java/org/springframework/security/config/HttpSecurityBeanDefinitionParser.java @@ -1,34 +1,32 @@ package org.springframework.security.config; -import org.springframework.beans.factory.xml.ParserContext; -import org.springframework.beans.factory.xml.BeanDefinitionParser; -import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.util.xml.DomUtils; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; -import org.springframework.security.util.FilterChainProxy; -import org.springframework.security.intercept.web.PathBasedFilterInvocationDefinitionMap; -import org.springframework.security.intercept.web.FilterSecurityInterceptor; -import org.springframework.security.intercept.web.FilterInvocationDefinitionMap; -import org.springframework.security.ConfigAttributeEditor; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; import org.springframework.security.ConfigAttributeDefinition; -import org.springframework.security.ui.ExceptionTranslationFilter; -import org.springframework.security.ui.webapp.AuthenticationProcessingFilter; -import org.springframework.security.ui.webapp.AuthenticationProcessingFilterEntryPoint; +import org.springframework.security.ConfigAttributeEditor; import org.springframework.security.context.HttpSessionContextIntegrationFilter; +import org.springframework.security.intercept.web.*; +import org.springframework.security.ui.ExceptionTranslationFilter; +import org.springframework.security.util.FilterChainProxy; +import org.springframework.security.util.RegexUrlPathMatcher; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.util.xml.DomUtils; import org.w3c.dom.Element; -import java.util.List; +import javax.servlet.Filter; import java.util.Iterator; +import java.util.List; /** * Sets up HTTP security: filter stack and protected URLs. * * - * @author luke + * @author Luke Taylor * @version $Id$ */ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser { @@ -39,39 +37,23 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser { public static final String DEFAULT_LOGOUT_FILTER_ID = "_logoutFilter"; public static final String DEFAULT_EXCEPTION_TRANSLATION_FILTER_ID = "_exceptionTranslationFilter"; public static final String DEFAULT_FILTER_SECURITY_INTERCEPTOR_ID = "_filterSecurityInterceptor"; - public static final String DEFAULT_FORM_LOGIN_FILTER_ID = "_formLoginFilter"; - public static final String DEFAULT_FORM_LOGIN_ENTRY_POINT_ID = "_formLoginEntryPoint"; public static final String LOGOUT_ELEMENT = "logout"; public static final String FORM_LOGIN_ELEMENT = "form-login"; + public static final String BASIC_AUTH_ELEMENT = "http-basic"; - private static final String PATH_ATTRIBUTE = "path"; - private static final String FILTERS_ATTRIBUTE = "filters"; - private static final String ACCESS_CONFIG_ATTRIBUTE = "access"; - - private static final String LOGIN_URL_ATTRIBUTE = "loginUrl"; + static final String PATH_PATTERN_ATTRIBUTE = "pattern"; + static final String PATTERN_TYPE_ATTRIBUTE = "pathType"; + static final String PATTERN_TYPE_REGEX = "regex"; - private static final String FORM_LOGIN_TARGET_URL_ATTRIBUTE = "defaultTargetUrl"; - private static final String DEFAULT_FORM_LOGIN_TARGET_URL = "/index"; + static final String FILTERS_ATTRIBUTE = "filters"; + static final String NO_FILTERS_VALUE = "none"; + static final Filter[] EMPTY_FILTER_CHAIN = new Filter[0]; - private static final String FORM_LOGIN_AUTH_FAILURE_URL_ATTRIBUTE = "defaultTargetUrl"; - // TODO: Change AbstractProcessingFilter to not need a failure URL and just write a failure message - // to the response if one isn't set. - private static final String DEFAULT_FORM_LOGIN_AUTH_FAILURE_URL = "/loginError"; + private static final String ACCESS_CONFIG_ATTRIBUTE = "access"; public BeanDefinition parse(Element element, ParserContext parserContext) { - // Create HttpSCIF, FilterInvocationInterceptor, ExceptionTranslationFilter - - // Find other filter beans. - - // Create appropriate bean list for config attributes to create FIDS - - // Add any secure URLs with specific filter chains to FIDS as separate ConfigAttributes - - // Add secure URLS with roles to objectDefinitionSource for FilterSecurityInterceptor - RootBeanDefinition filterChainProxy = new RootBeanDefinition(FilterChainProxy.class); - RootBeanDefinition httpSCIF = new RootBeanDefinition(HttpSessionContextIntegrationFilter.class); //TODO: Set session creation parameters based on session-creation attribute @@ -79,22 +61,28 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser { BeanDefinitionBuilder filterSecurityInterceptorBuilder = BeanDefinitionBuilder.rootBeanDefinition(FilterSecurityInterceptor.class); - BeanDefinitionBuilder exceptionTranslationFilterBuilder = BeanDefinitionBuilder.rootBeanDefinition(ExceptionTranslationFilter.class); // Autowire for entry point (for now) + // TODO: Examine entry point beans in post processing and pick the correct one + // i.e. form login or cas if defined, then any other non-basic, non-digest, then basic or digest exceptionTranslationFilterBuilder.setAutowireMode(RootBeanDefinition.AUTOWIRE_BY_TYPE); // TODO: Get path type attribute and determine FilDefInvS class - PathBasedFilterInvocationDefinitionMap filterChainInvocationDefSource - = new PathBasedFilterInvocationDefinitionMap(); - filterChainProxy.getPropertyValues().addPropertyValue("filterInvocationDefinitionSource", - filterChainInvocationDefSource); + FilterChainMap filterChainMap = new FilterChainMap(); + + String patternType = element.getAttribute(PATTERN_TYPE_ATTRIBUTE); + + FilterInvocationDefinitionMap interceptorFilterInvDefSource = new PathBasedFilterInvocationDefinitionMap(); + + if (patternType.equals(PATTERN_TYPE_REGEX)) { + filterChainMap.setUrlPathMatcher(new RegexUrlPathMatcher()); + interceptorFilterInvDefSource = new RegExpBasedFilterInvocationDefinitionMap(); + } - PathBasedFilterInvocationDefinitionMap interceptorFilterInvDefSource - = new PathBasedFilterInvocationDefinitionMap(); + filterChainProxy.getPropertyValues().addPropertyValue("filterChainMap", filterChainMap); filterSecurityInterceptorBuilder.addPropertyValue("objectDefinitionSource", interceptorFilterInvDefSource); @@ -102,7 +90,7 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser { filterSecurityInterceptorBuilder.setAutowireMode(RootBeanDefinition.AUTOWIRE_BY_TYPE); parseInterceptUrls(DomUtils.getChildElementsByTagName(element, "intercept-url"), - filterChainInvocationDefSource, interceptorFilterInvDefSource); + filterChainMap, interceptorFilterInvDefSource); // TODO: if empty, set a default set a default /**, omitting login url BeanDefinitionRegistry registry = parserContext.getRegistry(); @@ -110,51 +98,20 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser { Element logoutElt = DomUtils.getChildElementByTagName(element, LOGOUT_ELEMENT); if (logoutElt != null) { - BeanDefinition logoutFilter = new LogoutBeanDefinitionParser().parse(logoutElt, parserContext); + new LogoutBeanDefinitionParser().parse(logoutElt, parserContext); } Element formLoginElt = DomUtils.getChildElementByTagName(element, FORM_LOGIN_ELEMENT); if (formLoginElt != null) { - BeanDefinitionBuilder formLoginFilterBuilder = - BeanDefinitionBuilder.rootBeanDefinition(AuthenticationProcessingFilter.class); - BeanDefinitionBuilder formLoginEntryPointBuilder = - BeanDefinitionBuilder.rootBeanDefinition(AuthenticationProcessingFilterEntryPoint.class); - - // Temporary login value - formLoginEntryPointBuilder.addPropertyValue("loginFormUrl", "/login"); - - - String loginUrl = formLoginElt.getAttribute(LOGIN_URL_ATTRIBUTE); - - if (StringUtils.hasText(loginUrl)) { - formLoginFilterBuilder.addPropertyValue("filterProcessesUrl", loginUrl); - } - - String defaultTargetUrl = formLoginElt.getAttribute(FORM_LOGIN_TARGET_URL_ATTRIBUTE); - - if (!StringUtils.hasText(defaultTargetUrl)) { - defaultTargetUrl = DEFAULT_FORM_LOGIN_TARGET_URL; - } - - formLoginFilterBuilder.addPropertyValue("defaultTargetUrl", defaultTargetUrl); - - String authenticationFailureUrl = formLoginElt.getAttribute(FORM_LOGIN_AUTH_FAILURE_URL_ATTRIBUTE); - - if (!StringUtils.hasText(authenticationFailureUrl)) { - authenticationFailureUrl = DEFAULT_FORM_LOGIN_AUTH_FAILURE_URL; - } - - formLoginFilterBuilder.addPropertyValue("authenticationFailureUrl", authenticationFailureUrl); - // Set autowire to pick up the authentication manager. - formLoginFilterBuilder.setAutowireMode(RootBeanDefinition.AUTOWIRE_BY_TYPE); + new FormLoginBeanDefinitionParser().parse(formLoginElt, parserContext); + } + Element basicAuthElt = DomUtils.getChildElementByTagName(element, BASIC_AUTH_ELEMENT); - registry.registerBeanDefinition(DEFAULT_FORM_LOGIN_FILTER_ID, - formLoginFilterBuilder.getBeanDefinition()); - registry.registerBeanDefinition(DEFAULT_FORM_LOGIN_ENTRY_POINT_ID, - formLoginEntryPointBuilder.getBeanDefinition()); - } + if (basicAuthElt != null) { + new BasicAuthenticationBeanDefinitionParser().parse(basicAuthElt, parserContext); + } registry.registerBeanDefinition(DEFAULT_FILTER_CHAIN_PROXY_ID, filterChainProxy); registry.registerBeanDefinition(DEFAULT_HTTP_SESSION_FILTER_ID, httpSCIF); @@ -168,15 +125,16 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser { // app context has been created and all beans are available. registry.registerBeanDefinition("__httpConfigBeanFactoryPostProcessor", - new RootBeanDefinition(HttpSecurityConfigPostProcessor.class)); + new RootBeanDefinition(HttpSecurityConfigPostProcessor.class)); return null; } /** - * Parses the intercept-url elements and populates the FilterChainProxy's FilterInvocationDefinitionSource + * Parses the intercept-url elements and populates the FilterChainProxy's FilterChainMap and the + * FilterInvocationDefinitionSource used in FilterSecurityInterceptor. */ - private void parseInterceptUrls(List urlElts, FilterInvocationDefinitionMap filterChainInvocationDefSource, + private void parseInterceptUrls(List urlElts, FilterChainMap filterChainMap, FilterInvocationDefinitionMap interceptorFilterInvDefSource) { Iterator urlEltsIterator = urlElts.iterator(); @@ -186,7 +144,7 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser { while (urlEltsIterator.hasNext()) { Element urlElt = (Element) urlEltsIterator.next(); - String path = urlElt.getAttribute(PATH_ATTRIBUTE); + String path = urlElt.getAttribute(PATH_PATTERN_ATTRIBUTE); Assert.hasText(path, "path attribute cannot be empty or null"); @@ -204,11 +162,12 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser { String filters = urlElt.getAttribute(FILTERS_ATTRIBUTE); if (StringUtils.hasText(filters)) { - attributeEditor.setAsText(filters); - } - - + if (!filters.equals(NO_FILTERS_VALUE)) { + throw new IllegalStateException("Currently only 'none' is supported as the custom filters attribute"); + } + filterChainMap.addSecureUrl(path, EMPTY_FILTER_CHAIN); + } } } } diff --git a/core/src/main/java/org/springframework/security/config/HttpSecurityConfigPostProcessor.java b/core/src/main/java/org/springframework/security/config/HttpSecurityConfigPostProcessor.java index fe2a09dc07..01cee6494b 100644 --- a/core/src/main/java/org/springframework/security/config/HttpSecurityConfigPostProcessor.java +++ b/core/src/main/java/org/springframework/security/config/HttpSecurityConfigPostProcessor.java @@ -1,15 +1,22 @@ package org.springframework.security.config; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.beans.BeansException; -import org.springframework.security.util.FilterChainProxy; -import org.springframework.security.context.HttpSessionContextIntegrationFilter; +import org.springframework.core.OrderComparator; +import org.springframework.core.Ordered; import org.springframework.security.AuthenticationManager; +import org.springframework.security.context.HttpSessionContextIntegrationFilter; +import org.springframework.security.intercept.web.FilterChainMap; +import org.springframework.security.ui.AuthenticationEntryPoint; +import org.springframework.security.util.FilterChainProxy; import org.springframework.util.Assert; import javax.servlet.Filter; -import java.util.Map; +import java.util.*; /** * Responsible for tying up the HTTP security configuration - building ordered filter stack and linking up @@ -18,36 +25,96 @@ import java.util.Map; * @author Luke Taylor * @version $Id$ */ -public class HttpSecurityConfigPostProcessor implements BeanFactoryPostProcessor { - public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { - FilterChainProxy filterChainProxy = - (FilterChainProxy) beanFactory.getBean(HttpSecurityBeanDefinitionParser.DEFAULT_FILTER_CHAIN_PROXY_ID); +public class HttpSecurityConfigPostProcessor implements BeanFactoryPostProcessor, Ordered { + private Log logger = LogFactory.getLog(getClass()); + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { HttpSessionContextIntegrationFilter httpSCIF = (HttpSessionContextIntegrationFilter) beanFactory.getBean(HttpSecurityBeanDefinitionParser.DEFAULT_HTTP_SESSION_FILTER_ID); - AuthenticationManager authManager = (AuthenticationManager) getBeanOfType(AuthenticationManager.class, beanFactory); + configureAuthenticationEntryPoint(beanFactory); - // - Map filters = beanFactory.getBeansOfType(Filter.class); + configureFilterChain(beanFactory); + } + /** + * Selects the entry point that should be used in ExceptionTranslationFilter. Strategy is + * + *
    + *
  1. If only one use that.
  2. + *
  3. If more than one, check the default interactive login Ids in order of preference
  4. + *
  5. throw an exception (for now). TODO: Examine additional beans and types and make decision
  6. + *
+ * + * + * @param beanFactory + */ + private void configureAuthenticationEntryPoint(ConfigurableListableBeanFactory beanFactory) { + logger.info("Selecting AuthenticationEntryPoint for use in ExceptionTranslationFilter"); + + BeanDefinition etf = + beanFactory.getBeanDefinition(HttpSecurityBeanDefinitionParser.DEFAULT_EXCEPTION_TRANSLATION_FILTER_ID); + Map entryPointMap = beanFactory.getBeansOfType(AuthenticationEntryPoint.class); + List entryPoints = new ArrayList(entryPointMap.values()); + + Assert.isTrue(entryPoints.size() > 0, "No AuthenticationEntryPoint instances defined"); + + AuthenticationEntryPoint mainEntryPoint = (AuthenticationEntryPoint) + entryPointMap.get(FormLoginBeanDefinitionParser.DEFAULT_FORM_LOGIN_ENTRY_POINT_ID); + + if (mainEntryPoint == null) { + throw new SecurityConfigurationException("Failed to resolve authentication entry point"); + } + + logger.info("Main AuthenticationEntryPoint set to " + mainEntryPoint); + + etf.getPropertyValues().addPropertyValue("authenticationEntryPoint", mainEntryPoint); + } + private void configureFilterChain(ConfigurableListableBeanFactory beanFactory) { + FilterChainProxy filterChainProxy = + (FilterChainProxy) beanFactory.getBean(HttpSecurityBeanDefinitionParser.DEFAULT_FILTER_CHAIN_PROXY_ID); + // Set the default match + List defaultFilterChain = orderFilters(beanFactory); + FilterChainMap filterMap = filterChainProxy.getFilterChainMap(); + String allUrlsMatch = filterMap.getMatcher().getUniversalMatchPattern(); + filterMap.addSecureUrl(allUrlsMatch, (Filter[]) defaultFilterChain.toArray(new Filter[0])); + } + + private List orderFilters(ConfigurableListableBeanFactory beanFactory) { + Map filters = beanFactory.getBeansOfType(Filter.class); + Assert.notEmpty(filters, "No filters found in app context!"); + Iterator ids = filters.keySet().iterator(); - } + List orderedFilters = new ArrayList(); - private void configureFilterChain(ConfigurableListableBeanFactory beanFactory) { + while (ids.hasNext()) { + String id = (String) ids.next(); + Filter filter = (Filter) filters.get(id); + if (filter instanceof FilterChainProxy) { + continue; + } - } + if (!(filter instanceof Ordered)) { + // TODO: Possibly log this as a warning and skip this filter. + throw new IllegalArgumentException("Filter " + id + " must implement the Ordered interface"); + } + + orderedFilters.add(filter); + } + Collections.sort(orderedFilters, new OrderComparator()); + return orderedFilters; + } private Object getBeanOfType(Class clazz, ConfigurableListableBeanFactory beanFactory) { Map beans = beanFactory.getBeansOfType(clazz); @@ -56,4 +123,8 @@ public class HttpSecurityConfigPostProcessor implements BeanFactoryPostProcessor return beans.values().toArray()[0]; } + + public int getOrder() { + return 0; + } } diff --git a/core/src/main/java/org/springframework/security/ui/webapp/DefaultLoginPageGeneratingFilter.java b/core/src/main/java/org/springframework/security/ui/webapp/DefaultLoginPageGeneratingFilter.java new file mode 100644 index 0000000000..b1d010b981 --- /dev/null +++ b/core/src/main/java/org/springframework/security/ui/webapp/DefaultLoginPageGeneratingFilter.java @@ -0,0 +1,105 @@ +package org.springframework.security.ui.webapp; + +import org.springframework.security.AuthenticationException; +import org.springframework.security.ui.AbstractProcessingFilter; +import org.springframework.security.ui.FilterChainOrderUtils; +import org.springframework.security.ui.SpringSecurityFilter; +import org.springframework.security.ui.rememberme.TokenBasedRememberMeServices; +import org.springframework.util.StringUtils; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; + +/** + * For internal use with namespace configuration in the case where a user doesn't configure a login page. + * The configuration code will insert this filter in the chain instead. + * + * Will only work if a redirect is used to the login page. + * + * @author Luke Taylor + * @version $Id$ + */ +public class DefaultLoginPageGeneratingFilter extends SpringSecurityFilter { + public static final String DEFAULT_LOGIN_PAGE_URL = "/login"; + private String authenticationUrl; + private String usernameParameter; + private String passwordParameter; + private String rememberMeParameter; + + public DefaultLoginPageGeneratingFilter(AuthenticationProcessingFilter authFilter) { + authenticationUrl = authFilter.getDefaultFilterProcessesUrl(); + usernameParameter = authFilter.getUsernameParameter(); + passwordParameter = authFilter.getPasswordParameter(); + + if (authFilter.getRememberMeServices() instanceof TokenBasedRememberMeServices) { + rememberMeParameter = ((TokenBasedRememberMeServices)authFilter.getRememberMeServices()).getParameter(); + } + } + + protected void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { + if (isLoginUrlRequest(request)) { + response.getOutputStream().print(generateLoginPageHtml(request)); + + return; + } + + chain.doFilter(request, response); + } + + private String generateLoginPageHtml(HttpServletRequest request) { + boolean loginError = StringUtils.hasText(request.getParameter("login_error")); + String errorMsg = "none"; + String lastUser = ""; + + if (loginError) { + HttpSession session = request.getSession(false); + + if(session != null) { + errorMsg = ((AuthenticationException) + session.getAttribute(AbstractProcessingFilter.SPRING_SECURITY_LAST_EXCEPTION_KEY)).getMessage(); + } + } + + return "Login Page\n" + + (loginError ? ("Your login attempt was not successful, try again.

Reason: " + + errorMsg + "
") : "") + + "
\n" + + " \n" + + " \n" + + " \n" + + + (rememberMeParameter == null ? "" : + " \n" + ) + + " \n" + + " \n" + + "
User:
Password:
Remember me on this computer.
\n" + + "
"; + } + + public int getOrder() { + return FilterChainOrderUtils.LOGIN_PAGE_FILTER_ORDER; + } + + private boolean isLoginUrlRequest(HttpServletRequest request) { + String uri = request.getRequestURI(); + int pathParamIndex = uri.indexOf(';'); + + if (pathParamIndex > 0) { + // strip everything after the first semi-colon + uri = uri.substring(0, pathParamIndex); + } + + if ("".equals(request.getContextPath())) { + return uri.endsWith(DEFAULT_LOGIN_PAGE_URL); + } + + return uri.endsWith(request.getContextPath() + DEFAULT_LOGIN_PAGE_URL); + } +} diff --git a/core/src/test/java/org/springframework/security/config/HttpSecurityBeanDefinitionParserTests.java b/core/src/test/java/org/springframework/security/config/HttpSecurityBeanDefinitionParserTests.java index 87a15adc92..e8888b1e22 100644 --- a/core/src/test/java/org/springframework/security/config/HttpSecurityBeanDefinitionParserTests.java +++ b/core/src/test/java/org/springframework/security/config/HttpSecurityBeanDefinitionParserTests.java @@ -1,8 +1,21 @@ package org.springframework.security.config; -import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.junit.AfterClass; +import static org.junit.Assert.assertTrue; import org.junit.BeforeClass; import org.junit.Test; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.security.context.HttpSessionContextIntegrationFilter; +import org.springframework.security.intercept.web.FilterChainMap; +import org.springframework.security.intercept.web.FilterSecurityInterceptor; +import org.springframework.security.ui.ExceptionTranslationFilter; +import org.springframework.security.ui.basicauth.BasicProcessingFilter; +import org.springframework.security.ui.logout.LogoutFilter; +import org.springframework.security.ui.webapp.AuthenticationProcessingFilter; +import org.springframework.security.ui.webapp.DefaultLoginPageGeneratingFilter; +import org.springframework.security.util.FilterChainProxy; + +import javax.servlet.Filter; /** * @author Luke Taylor @@ -16,8 +29,44 @@ public class HttpSecurityBeanDefinitionParserTests { appContext = new ClassPathXmlApplicationContext("org/springframework/security/config/http-security.xml"); } + @AfterClass + public static void closeAppContext() { + if (appContext != null) { + appContext.close(); + } + } + @Test - public void testContextContainsExpectedBeansAndData() { + public void filterChainProxyShouldReturnEmptyFilterListForUnprotectedUrl() { + FilterChainProxy filterChainProxy = + (FilterChainProxy) appContext.getBean(HttpSecurityBeanDefinitionParser.DEFAULT_FILTER_CHAIN_PROXY_ID); + + FilterChainMap filterChainMap = filterChainProxy.getFilterChainMap(); + + Filter[] filters = filterChainMap.getFilters("/unprotected"); + + assertTrue(filters.length == 0); } + @Test + public void filterChainProxyShouldReturnCorrectFilterListForProtectedUrl() { + FilterChainProxy filterChainProxy = + (FilterChainProxy) appContext.getBean(HttpSecurityBeanDefinitionParser.DEFAULT_FILTER_CHAIN_PROXY_ID); + + FilterChainMap filterChainMap = filterChainProxy.getFilterChainMap(); + + Filter[] filters = filterChainMap.getFilters("/someurl"); + + + + assertTrue("Expected 7 filters in chain", filters.length == 7); + + assertTrue(filters[0] instanceof HttpSessionContextIntegrationFilter); + assertTrue(filters[1] instanceof LogoutFilter); + assertTrue(filters[2] instanceof AuthenticationProcessingFilter); + assertTrue(filters[3] instanceof DefaultLoginPageGeneratingFilter); + assertTrue(filters[4] instanceof BasicProcessingFilter); + assertTrue(filters[5] instanceof ExceptionTranslationFilter); + assertTrue(filters[6] instanceof FilterSecurityInterceptor); + } } diff --git a/core/src/test/resources/org/springframework/security/config/http-security.xml b/core/src/test/resources/org/springframework/security/config/http-security.xml index 3c314c0143..616f87f682 100644 --- a/core/src/test/resources/org/springframework/security/config/http-security.xml +++ b/core/src/test/resources/org/springframework/security/config/http-security.xml @@ -6,20 +6,22 @@ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.xsd"> - + - - - + + + + + + - - - - + + + @@ -27,6 +29,6 @@ http://www.springframework.org/schema/security http://www.springframework.org/sc - + \ No newline at end of file