/* * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.security.config.http; import java.util.*; import javax.servlet.Filter; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.web.DefaultSecurityFilterChain; import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.access.ExceptionTranslationFilter; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.context.SecurityContextPersistenceFilter; import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter; import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter; import org.springframework.security.web.session.SessionManagementFilter; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.security.web.util.matcher.AnyRequestMatcher; public class DefaultFilterChainValidator implements FilterChainProxy.FilterChainValidator { private final Log logger = LogFactory.getLog(getClass()); public void validate(FilterChainProxy fcp) { for (SecurityFilterChain filterChain : fcp.getFilterChains()) { checkLoginPageIsntProtected(fcp, filterChain.getFilters()); checkFilterStack(filterChain.getFilters()); } checkPathOrder(new ArrayList<SecurityFilterChain>(fcp.getFilterChains())); checkForDuplicateMatchers(new ArrayList<SecurityFilterChain>( fcp.getFilterChains())); } private void checkPathOrder(List<SecurityFilterChain> filterChains) { // Check that the universal pattern is listed at the end, if at all Iterator<SecurityFilterChain> chains = filterChains.iterator(); while (chains.hasNext()) { RequestMatcher matcher = ((DefaultSecurityFilterChain) chains.next()) .getRequestMatcher(); if (AnyRequestMatcher.INSTANCE.equals(matcher) && chains.hasNext()) { throw new IllegalArgumentException( "A universal match pattern ('/**') is defined " + " before other patterns in the filter chain, causing them to be ignored. Please check the " + "ordering in your <security:http> namespace or FilterChainProxy bean configuration"); } } } private void checkForDuplicateMatchers(List<SecurityFilterChain> chains) { while (chains.size() > 1) { DefaultSecurityFilterChain chain = (DefaultSecurityFilterChain) chains .remove(0); for (SecurityFilterChain test : chains) { if (chain.getRequestMatcher().equals( ((DefaultSecurityFilterChain) test).getRequestMatcher())) { throw new IllegalArgumentException( "The FilterChainProxy contains two filter chains using the" + " matcher " + chain.getRequestMatcher() + ". If you are using multiple <http> namespace " + "elements, you must use a 'pattern' attribute to define the request patterns to which they apply."); } } } } @SuppressWarnings({ "unchecked" }) private <F extends Filter> F getFilter(Class<F> type, List<Filter> filters) { for (Filter f : filters) { if (type.isAssignableFrom(f.getClass())) { return (F) f; } } return null; } /** * Checks the filter list for possible errors and logs them */ private void checkFilterStack(List<Filter> filters) { checkForDuplicates(SecurityContextPersistenceFilter.class, filters); checkForDuplicates(UsernamePasswordAuthenticationFilter.class, filters); checkForDuplicates(SessionManagementFilter.class, filters); checkForDuplicates(BasicAuthenticationFilter.class, filters); checkForDuplicates(SecurityContextHolderAwareRequestFilter.class, filters); checkForDuplicates(JaasApiIntegrationFilter.class, filters); checkForDuplicates(ExceptionTranslationFilter.class, filters); checkForDuplicates(FilterSecurityInterceptor.class, filters); } private void checkForDuplicates(Class<? extends Filter> clazz, List<Filter> filters) { for (int i = 0; i < filters.size(); i++) { Filter f1 = filters.get(i); if (clazz.isAssignableFrom(f1.getClass())) { // Found the first one, check remaining for another for (int j = i + 1; j < filters.size(); j++) { Filter f2 = filters.get(j); if (clazz.isAssignableFrom(f2.getClass())) { logger.warn("Possible error: Filters at position " + i + " and " + j + " are both " + "instances of " + clazz.getName()); return; } } } } } /* * Checks for the common error of having a login page URL protected by the security * interceptor */ private void checkLoginPageIsntProtected(FilterChainProxy fcp, List<Filter> filterStack) { ExceptionTranslationFilter etf = getFilter(ExceptionTranslationFilter.class, filterStack); if (etf == null || !(etf.getAuthenticationEntryPoint() instanceof LoginUrlAuthenticationEntryPoint)) { return; } String loginPage = ((LoginUrlAuthenticationEntryPoint) etf .getAuthenticationEntryPoint()).getLoginFormUrl(); logger.info("Checking whether login URL '" + loginPage + "' is accessible with your configuration"); FilterInvocation loginRequest = new FilterInvocation(loginPage, "POST"); List<Filter> filters = null; try { filters = fcp.getFilters(loginPage); } catch (Exception e) { // May happen legitimately if a filter-chain request matcher requires more // request data than that provided // by the dummy request used when creating the filter invocation. logger.info("Failed to obtain filter chain information for the login page. Unable to complete check."); } if (filters == null || filters.isEmpty()) { logger.debug("Filter chain is empty for the login page"); return; } if (getFilter(DefaultLoginPageGeneratingFilter.class, filters) != null) { logger.debug("Default generated login page is in use"); return; } FilterSecurityInterceptor fsi = getFilter(FilterSecurityInterceptor.class, filters); FilterInvocationSecurityMetadataSource fids = fsi.getSecurityMetadataSource(); Collection<ConfigAttribute> attributes = fids.getAttributes(loginRequest); if (attributes == null) { logger.debug("No access attributes defined for login page URL"); if (fsi.isRejectPublicInvocations()) { logger.warn("FilterSecurityInterceptor is configured to reject public invocations." + " Your login page may not be accessible."); } return; } AnonymousAuthenticationFilter anonPF = getFilter( AnonymousAuthenticationFilter.class, filters); if (anonPF == null) { logger.warn("The login page is being protected by the filter chain, but you don't appear to have" + " anonymous authentication enabled. This is almost certainly an error."); return; } // Simulate an anonymous access with the supplied attributes. AnonymousAuthenticationToken token = new AnonymousAuthenticationToken("key", anonPF.getPrincipal(), anonPF.getAuthorities()); try { fsi.getAccessDecisionManager().decide(token, loginRequest, attributes); } catch (AccessDeniedException e) { logger.warn("Anonymous access to the login page doesn't appear to be enabled. This is almost certainly " + "an error. Please check your configuration allows unauthenticated access to the configured " + "login page. (Simulated access was rejected: " + e + ")"); } catch (Exception e) { // May happen legitimately if a filter-chain request matcher requires more // request data than that provided // by the dummy request used when creating the filter invocation. See SEC-1878 logger.info( "Unable to check access to the login page to determine if anonymous access is allowed. This might be an error, but can happen under normal circumstances.", e); } } }