/*
* 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;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanMetadataElement;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanReference;
import org.springframework.beans.factory.config.ListFactoryBean;
import org.springframework.beans.factory.config.MethodInvokingFactoryBean;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.parsing.BeanComponentDefinition;
import org.springframework.beans.factory.parsing.CompositeComponentDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.core.OrderComparator;
import org.springframework.core.Ordered;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.Elements;
import org.springframework.security.config.authentication.AuthenticationManagerFactoryBean;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.PortResolverImpl;
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Element;
import java.util.*;
/**
* Sets up HTTP security: filter stack and protected URLs.
*
* @author Luke Taylor
* @author Ben Alex
* @author Rob Winch
* @since 2.0
*/
public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser {
private static final Log logger = LogFactory
.getLog(HttpSecurityBeanDefinitionParser.class);
private static final String ATT_AUTHENTICATION_MANAGER_REF = "authentication-manager-ref";
static final String ATT_REQUEST_MATCHER_REF = "request-matcher-ref";
static final String ATT_PATH_PATTERN = "pattern";
static final String ATT_HTTP_METHOD = "method";
static final String ATT_FILTERS = "filters";
static final String OPT_FILTERS_NONE = "none";
static final String ATT_REQUIRES_CHANNEL = "requires-channel";
private static final String ATT_REF = "ref";
private static final String ATT_SECURED = "security";
private static final String OPT_SECURITY_NONE = "none";
public HttpSecurityBeanDefinitionParser() {
}
/**
* The aim of this method is to build the list of filters which have been defined by
* the namespace elements and attributes within the <http> configuration, along
* with any custom-filter's linked to user-defined filter beans.
* <p>
* By the end of this method, the default <tt>FilterChainProxy</tt> bean should have
* been registered and will have the map of filter chains defined, with the
* "universal" match pattern mapped to the list of beans which have been parsed here.
*/
@SuppressWarnings({ "unchecked" })
public BeanDefinition parse(Element element, ParserContext pc) {
CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(
element.getTagName(), pc.extractSource(element));
pc.pushContainingComponent(compositeDef);
registerFilterChainProxyIfNecessary(pc, pc.extractSource(element));
// Obtain the filter chains and add the new chain to it
BeanDefinition listFactoryBean = pc.getRegistry().getBeanDefinition(
BeanIds.FILTER_CHAINS);
List<BeanReference> filterChains = (List<BeanReference>) listFactoryBean
.getPropertyValues().getPropertyValue("sourceList").getValue();
filterChains.add(createFilterChain(element, pc));
pc.popAndRegisterContainingComponent();
return null;
}
/**
* Creates the {@code SecurityFilterChain} bean from an <http> element.
*/
private BeanReference createFilterChain(Element element, ParserContext pc) {
boolean secured = !OPT_SECURITY_NONE.equals(element.getAttribute(ATT_SECURED));
if (!secured) {
if (!StringUtils.hasText(element.getAttribute(ATT_PATH_PATTERN))
&& !StringUtils.hasText(ATT_REQUEST_MATCHER_REF)) {
pc.getReaderContext().error(
"The '" + ATT_SECURED
+ "' attribute must be used in combination with"
+ " the '" + ATT_PATH_PATTERN + "' or '"
+ ATT_REQUEST_MATCHER_REF + "' attributes.",
pc.extractSource(element));
}
for (int n = 0; n < element.getChildNodes().getLength(); n++) {
if (element.getChildNodes().item(n) instanceof Element) {
pc.getReaderContext().error(
"If you are using <http> to define an unsecured pattern, "
+ "it cannot contain child elements.",
pc.extractSource(element));
}
}
return createSecurityFilterChainBean(element, pc, Collections.emptyList());
}
final BeanReference portMapper = createPortMapper(element, pc);
final BeanReference portResolver = createPortResolver(portMapper, pc);
ManagedList<BeanReference> authenticationProviders = new ManagedList<BeanReference>();
BeanReference authenticationManager = createAuthenticationManager(element, pc,
authenticationProviders);
boolean forceAutoConfig = isDefaultHttpConfig(element);
HttpConfigurationBuilder httpBldr = new HttpConfigurationBuilder(element,
forceAutoConfig, pc, portMapper, portResolver, authenticationManager);
AuthenticationConfigBuilder authBldr = new AuthenticationConfigBuilder(element,
forceAutoConfig, pc, httpBldr.getSessionCreationPolicy(),
httpBldr.getRequestCache(), authenticationManager,
httpBldr.getSessionStrategy(), portMapper, portResolver,
httpBldr.getCsrfLogoutHandler());
httpBldr.setLogoutHandlers(authBldr.getLogoutHandlers());
httpBldr.setEntryPoint(authBldr.getEntryPointBean());
httpBldr.setAccessDeniedHandler(authBldr.getAccessDeniedHandlerBean());
authenticationProviders.addAll(authBldr.getProviders());
List<OrderDecorator> unorderedFilterChain = new ArrayList<OrderDecorator>();
unorderedFilterChain.addAll(httpBldr.getFilters());
unorderedFilterChain.addAll(authBldr.getFilters());
unorderedFilterChain.addAll(buildCustomFilterList(element, pc));
Collections.sort(unorderedFilterChain, new OrderComparator());
checkFilterChainOrder(unorderedFilterChain, pc, pc.extractSource(element));
// The list of filter beans
List<BeanMetadataElement> filterChain = new ManagedList<BeanMetadataElement>();
for (OrderDecorator od : unorderedFilterChain) {
filterChain.add(od.bean);
}
return createSecurityFilterChainBean(element, pc, filterChain);
}
private static boolean isDefaultHttpConfig(Element httpElt) {
return httpElt.getChildNodes().getLength() == 0
&& httpElt.getAttributes().getLength() == 0;
}
private BeanReference createSecurityFilterChainBean(Element element,
ParserContext pc, List<?> filterChain) {
BeanMetadataElement filterChainMatcher;
String requestMatcherRef = element.getAttribute(ATT_REQUEST_MATCHER_REF);
String filterChainPattern = element.getAttribute(ATT_PATH_PATTERN);
if (StringUtils.hasText(requestMatcherRef)) {
if (StringUtils.hasText(filterChainPattern)) {
pc.getReaderContext().error(
"You can't define a pattern and a request-matcher-ref for the "
+ "same filter chain", pc.extractSource(element));
}
filterChainMatcher = new RuntimeBeanReference(requestMatcherRef);
}
else if (StringUtils.hasText(filterChainPattern)) {
filterChainMatcher = MatcherType.fromElement(element).createMatcher(pc,
filterChainPattern, null);
}
else {
filterChainMatcher = new RootBeanDefinition(AnyRequestMatcher.class);
}
BeanDefinitionBuilder filterChainBldr = BeanDefinitionBuilder
.rootBeanDefinition(DefaultSecurityFilterChain.class);
filterChainBldr.addConstructorArgValue(filterChainMatcher);
filterChainBldr.addConstructorArgValue(filterChain);
BeanDefinition filterChainBean = filterChainBldr.getBeanDefinition();
String id = element.getAttribute("name");
if (!StringUtils.hasText(id)) {
id = element.getAttribute("id");
if (!StringUtils.hasText(id)) {
id = pc.getReaderContext().generateBeanName(filterChainBean);
}
}
pc.registerBeanComponent(new BeanComponentDefinition(filterChainBean, id));
return new RuntimeBeanReference(id);
}
private BeanReference createPortMapper(Element elt, ParserContext pc) {
// Register the portMapper. A default will always be created, even if no element
// exists.
BeanDefinition portMapper = new PortMappingsBeanDefinitionParser().parse(
DomUtils.getChildElementByTagName(elt, Elements.PORT_MAPPINGS), pc);
String portMapperName = pc.getReaderContext().generateBeanName(portMapper);
pc.registerBeanComponent(new BeanComponentDefinition(portMapper, portMapperName));
return new RuntimeBeanReference(portMapperName);
}
private RuntimeBeanReference createPortResolver(BeanReference portMapper,
ParserContext pc) {
RootBeanDefinition portResolver = new RootBeanDefinition(PortResolverImpl.class);
portResolver.getPropertyValues().addPropertyValue("portMapper", portMapper);
String portResolverName = pc.getReaderContext().generateBeanName(portResolver);
pc.registerBeanComponent(new BeanComponentDefinition(portResolver,
portResolverName));
return new RuntimeBeanReference(portResolverName);
}
/**
* Creates the internal AuthenticationManager bean which uses either the externally
* registered (global) one as a parent or the bean specified by
* "authentication-manager-ref".
*
* All the providers registered by this <http> block will be registered with the
* internal authentication manager.
*/
private BeanReference createAuthenticationManager(Element element, ParserContext pc,
ManagedList<BeanReference> authenticationProviders) {
String parentMgrRef = element.getAttribute(ATT_AUTHENTICATION_MANAGER_REF);
BeanDefinitionBuilder authManager = BeanDefinitionBuilder
.rootBeanDefinition(ProviderManager.class);
authManager.addConstructorArgValue(authenticationProviders);
if (StringUtils.hasText(parentMgrRef)) {
RuntimeBeanReference parentAuthManager = new RuntimeBeanReference(
parentMgrRef);
authManager.addConstructorArgValue(parentAuthManager);
RootBeanDefinition clearCredentials = new RootBeanDefinition(
ClearCredentialsMethodInvokingFactoryBean.class);
clearCredentials.getPropertyValues().addPropertyValue("targetObject",
parentAuthManager);
clearCredentials.getPropertyValues().addPropertyValue("targetMethod",
"isEraseCredentialsAfterAuthentication");
authManager.addPropertyValue("eraseCredentialsAfterAuthentication",
clearCredentials);
}
else {
RootBeanDefinition amfb = new RootBeanDefinition(
AuthenticationManagerFactoryBean.class);
amfb.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
String amfbId = pc.getReaderContext().generateBeanName(amfb);
pc.registerBeanComponent(new BeanComponentDefinition(amfb, amfbId));
RootBeanDefinition clearCredentials = new RootBeanDefinition(
MethodInvokingFactoryBean.class);
clearCredentials.getPropertyValues().addPropertyValue("targetObject",
new RuntimeBeanReference(amfbId));
clearCredentials.getPropertyValues().addPropertyValue("targetMethod",
"isEraseCredentialsAfterAuthentication");
authManager.addConstructorArgValue(new RuntimeBeanReference(amfbId));
authManager.addPropertyValue("eraseCredentialsAfterAuthentication",
clearCredentials);
}
authManager.getRawBeanDefinition().setSource(pc.extractSource(element));
BeanDefinition authMgrBean = authManager.getBeanDefinition();
String id = pc.getReaderContext().generateBeanName(authMgrBean);
pc.registerBeanComponent(new BeanComponentDefinition(authMgrBean, id));
return new RuntimeBeanReference(id);
}
private void checkFilterChainOrder(List<OrderDecorator> filters, ParserContext pc,
Object source) {
logger.info("Checking sorted filter chain: " + filters);
for (int i = 0; i < filters.size(); i++) {
OrderDecorator filter = filters.get(i);
if (i > 0) {
OrderDecorator previous = filters.get(i - 1);
if (filter.getOrder() == previous.getOrder()) {
pc.getReaderContext()
.error("Filter beans '"
+ filter.bean
+ "' and '"
+ previous.bean
+ "' have the same 'order' value. When using custom filters, "
+ "please make sure the positions do not conflict with default filters. "
+ "Alternatively you can disable the default filters by removing the corresponding "
+ "child elements from <http> and avoiding the use of <http auto-config='true'>.",
source);
}
}
}
}
List<OrderDecorator> buildCustomFilterList(Element element, ParserContext pc) {
List<Element> customFilterElts = DomUtils.getChildElementsByTagName(element,
Elements.CUSTOM_FILTER);
List<OrderDecorator> customFilters = new ArrayList<OrderDecorator>();
final String ATT_AFTER = "after";
final String ATT_BEFORE = "before";
final String ATT_POSITION = "position";
for (Element elt : customFilterElts) {
String after = elt.getAttribute(ATT_AFTER);
String before = elt.getAttribute(ATT_BEFORE);
String position = elt.getAttribute(ATT_POSITION);
String ref = elt.getAttribute(ATT_REF);
if (!StringUtils.hasText(ref)) {
pc.getReaderContext().error(
"The '" + ATT_REF + "' attribute must be supplied",
pc.extractSource(elt));
}
RuntimeBeanReference bean = new RuntimeBeanReference(ref);
if (WebConfigUtils.countNonEmpty(new String[] { after, before, position }) != 1) {
pc.getReaderContext().error(
"A single '" + ATT_AFTER + "', '" + ATT_BEFORE + "', or '"
+ ATT_POSITION + "' attribute must be supplied",
pc.extractSource(elt));
}
if (StringUtils.hasText(position)) {
customFilters.add(new OrderDecorator(bean, SecurityFilters
.valueOf(position)));
}
else if (StringUtils.hasText(after)) {
SecurityFilters order = SecurityFilters.valueOf(after);
if (order == SecurityFilters.LAST) {
customFilters.add(new OrderDecorator(bean, SecurityFilters.LAST));
}
else {
customFilters.add(new OrderDecorator(bean, order.getOrder() + 1));
}
}
else if (StringUtils.hasText(before)) {
SecurityFilters order = SecurityFilters.valueOf(before);
if (order == SecurityFilters.FIRST) {
customFilters.add(new OrderDecorator(bean, SecurityFilters.FIRST));
}
else {
customFilters.add(new OrderDecorator(bean, order.getOrder() - 1));
}
}
}
return customFilters;
}
static void registerFilterChainProxyIfNecessary(ParserContext pc, Object source) {
if (pc.getRegistry().containsBeanDefinition(BeanIds.FILTER_CHAIN_PROXY)) {
return;
}
// Not already registered, so register the list of filter chains and the
// FilterChainProxy
BeanDefinition listFactoryBean = new RootBeanDefinition(ListFactoryBean.class);
listFactoryBean.getPropertyValues().add("sourceList", new ManagedList());
pc.registerBeanComponent(new BeanComponentDefinition(listFactoryBean,
BeanIds.FILTER_CHAINS));
BeanDefinitionBuilder fcpBldr = BeanDefinitionBuilder
.rootBeanDefinition(FilterChainProxy.class);
fcpBldr.getRawBeanDefinition().setSource(source);
fcpBldr.addConstructorArgReference(BeanIds.FILTER_CHAINS);
fcpBldr.addPropertyValue("filterChainValidator", new RootBeanDefinition(
DefaultFilterChainValidator.class));
BeanDefinition fcpBean = fcpBldr.getBeanDefinition();
pc.registerBeanComponent(new BeanComponentDefinition(fcpBean,
BeanIds.FILTER_CHAIN_PROXY));
pc.getRegistry().registerAlias(BeanIds.FILTER_CHAIN_PROXY,
BeanIds.SPRING_SECURITY_FILTER_CHAIN);
}
}
class OrderDecorator implements Ordered {
final BeanMetadataElement bean;
final int order;
public OrderDecorator(BeanMetadataElement bean, SecurityFilters filterOrder) {
this.bean = bean;
this.order = filterOrder.getOrder();
}
public OrderDecorator(BeanMetadataElement bean, int order) {
this.bean = bean;
this.order = order;
}
public int getOrder() {
return order;
}
public String toString() {
return bean + ", order = " + order;
}
}
/**
* Custom {@link MethodInvokingFactoryBean} that is specifically used for looking up the
* child {@link ProviderManager} value for
* {@link ProviderManager#setEraseCredentialsAfterAuthentication(boolean)} given the
* parent {@link AuthenticationManager}. This is necessary because the parent
* {@link AuthenticationManager} might not be a {@link ProviderManager}.
*
* @author Rob Winch
*/
final class ClearCredentialsMethodInvokingFactoryBean extends MethodInvokingFactoryBean {
public void afterPropertiesSet() throws Exception {
boolean isTargetProviderManager = getTargetObject() instanceof ProviderManager;
if (!isTargetProviderManager) {
setTargetObject(this);
}
super.afterPropertiesSet();
}
/**
* The default value if the target object is not a ProviderManager is false. We use
* false because this feature is associated with {@link ProviderManager} not
* {@link AuthenticationManager}. If the user wants to leverage
* {@link ProviderManager#setEraseCredentialsAfterAuthentication(boolean)} their
* original {@link AuthenticationManager} must be a {@link ProviderManager} (we should
* not magically add this functionality to their implementation since we cannot
* determine if it should be on or off).
*
* @return
*/
public boolean isEraseCredentialsAfterAuthentication() {
return false;
}
}