mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-07-04 01:32:14 +00:00
SEC-1171: Allow multiple http elements and add pattern attribute to specify filter chain mapping.
This commit is contained in:
parent
b0758dd8de
commit
7d74b7c87e
@ -22,21 +22,18 @@ import org.springframework.security.web.authentication.www.BasicAuthenticationFi
|
|||||||
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
|
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
|
||||||
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter;
|
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter;
|
||||||
import org.springframework.security.web.session.SessionManagementFilter;
|
import org.springframework.security.web.session.SessionManagementFilter;
|
||||||
import org.springframework.security.web.util.AnyRequestMatcher;
|
|
||||||
|
|
||||||
public class DefaultFilterChainValidator implements FilterChainProxy.FilterChainValidator {
|
public class DefaultFilterChainValidator implements FilterChainProxy.FilterChainValidator {
|
||||||
private Log logger = LogFactory.getLog(getClass());
|
private Log logger = LogFactory.getLog(getClass());
|
||||||
|
|
||||||
public void validate(FilterChainProxy fcp) {
|
public void validate(FilterChainProxy fcp) {
|
||||||
for(List<Filter> filters : fcp.getFilterChainMap().values()) {
|
for(List<Filter> filters : fcp.getFilterChainMap().values()) {
|
||||||
|
checkLoginPageIsntProtected(fcp, filters);
|
||||||
checkFilterStack(filters);
|
checkFilterStack(filters);
|
||||||
}
|
}
|
||||||
|
|
||||||
checkLoginPageIsntProtected(fcp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Object getFilter(Class<?> type, List<Filter> filters) {
|
private Object getFilter(Class<?> type, List<Filter> filters) {
|
||||||
|
|
||||||
for (Filter f : filters) {
|
for (Filter f : filters) {
|
||||||
if (type.isAssignableFrom(f.getClass())) {
|
if (type.isAssignableFrom(f.getClass())) {
|
||||||
return f;
|
return f;
|
||||||
@ -77,13 +74,14 @@ public class DefaultFilterChainValidator implements FilterChainProxy.FilterChain
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Checks for the common error of having a login page URL protected by the security interceptor */
|
/* Checks for the common error of having a login page URL protected by the security interceptor */
|
||||||
private void checkLoginPageIsntProtected(FilterChainProxy fcp) {
|
private void checkLoginPageIsntProtected(FilterChainProxy fcp, List<Filter> filterStack) {
|
||||||
List<Filter> defaultFilters = fcp.getFilterChainMap().get(new AnyRequestMatcher());
|
ExceptionTranslationFilter etf = (ExceptionTranslationFilter)getFilter(ExceptionTranslationFilter.class, filterStack);
|
||||||
ExceptionTranslationFilter etf = (ExceptionTranslationFilter)getFilter(ExceptionTranslationFilter.class, defaultFilters);
|
|
||||||
|
|
||||||
if (etf.getAuthenticationEntryPoint() instanceof LoginUrlAuthenticationEntryPoint) {
|
if(etf == null || !(etf.getAuthenticationEntryPoint() instanceof LoginUrlAuthenticationEntryPoint)) {
|
||||||
String loginPage =
|
return;
|
||||||
((LoginUrlAuthenticationEntryPoint)etf.getAuthenticationEntryPoint()).getLoginFormUrl();
|
}
|
||||||
|
|
||||||
|
String loginPage = ((LoginUrlAuthenticationEntryPoint)etf.getAuthenticationEntryPoint()).getLoginFormUrl();
|
||||||
FilterInvocation loginRequest = new FilterInvocation(loginPage, "POST");
|
FilterInvocation loginRequest = new FilterInvocation(loginPage, "POST");
|
||||||
List<Filter> filters = fcp.getFilters(loginPage);
|
List<Filter> filters = fcp.getFilters(loginPage);
|
||||||
logger.info("Checking whether login URL '" + loginPage + "' is accessible with your configuration");
|
logger.info("Checking whether login URL '" + loginPage + "' is accessible with your configuration");
|
||||||
@ -131,5 +129,5 @@ public class DefaultFilterChainValidator implements FilterChainProxy.FilterChain
|
|||||||
"login page. (Simulated access was rejected: " + e + ")");
|
"login page. (Simulated access was rejected: " + e + ")");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ 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.RuntimeBeanReference;
|
import org.springframework.beans.factory.config.RuntimeBeanReference;
|
||||||
|
import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder;
|
||||||
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;
|
||||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||||
@ -104,9 +105,20 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser {
|
|||||||
filterChain.add(od.bean);
|
filterChain.add(od.bean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the map of empty filter chains (if any)
|
||||||
ManagedMap<BeanDefinition, List<BeanMetadataElement>> filterChainMap = httpBldr.getFilterChainMap();
|
ManagedMap<BeanDefinition, List<BeanMetadataElement>> filterChainMap = httpBldr.getFilterChainMap();
|
||||||
BeanDefinition universalMatch = new RootBeanDefinition(AnyRequestMatcher.class);
|
|
||||||
filterChainMap.put(universalMatch, filterChain);
|
String filterChainPattern = element.getAttribute(ATT_PATH_PATTERN);
|
||||||
|
|
||||||
|
BeanDefinition filterChainMatcher;
|
||||||
|
|
||||||
|
if (StringUtils.hasText(filterChainPattern)) {
|
||||||
|
filterChainMatcher = matcherType.createMatcher(filterChainPattern, null);
|
||||||
|
} else {
|
||||||
|
filterChainMatcher = new RootBeanDefinition(AnyRequestMatcher.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
filterChainMap.put(filterChainMatcher, filterChain);
|
||||||
|
|
||||||
registerFilterChainProxy(pc, filterChainMap, source);
|
registerFilterChainProxy(pc, filterChainMap, source);
|
||||||
|
|
||||||
@ -215,19 +227,34 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser {
|
|||||||
return customFilters;
|
return customFilters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
private void registerFilterChainProxy(ParserContext pc, Map<BeanDefinition, List<BeanMetadataElement>> filterChainMap, Object source) {
|
private void registerFilterChainProxy(ParserContext pc, Map<BeanDefinition, List<BeanMetadataElement>> filterChainMap, Object source) {
|
||||||
if (pc.getRegistry().containsBeanDefinition(BeanIds.FILTER_CHAIN_PROXY)) {
|
if (pc.getRegistry().containsBeanDefinition(BeanIds.FILTER_CHAIN_PROXY)) {
|
||||||
pc.getReaderContext().error("Duplicate <http> element detected", source);
|
// Already registered. Obtain the filter chain map and add the new entries to it
|
||||||
}
|
// pc.getReaderContext().error("Duplicate <http> element detected", source);
|
||||||
|
|
||||||
|
BeanDefinition fcp = pc.getRegistry().getBeanDefinition(BeanIds.FILTER_CHAIN_PROXY);
|
||||||
|
Map existingFilterChainMap = (Map) fcp.getPropertyValues().getPropertyValue("filterChainMap").getValue();
|
||||||
|
|
||||||
|
for (BeanDefinition matcherBean : filterChainMap.keySet()) {
|
||||||
|
if (existingFilterChainMap.containsKey(matcherBean)) {
|
||||||
|
Map<Integer,ValueHolder> args = matcherBean.getConstructorArgumentValues().getIndexedArgumentValues();
|
||||||
|
pc.getReaderContext().error("The filter chain map already contains this request matcher ["
|
||||||
|
+ args.get(0).getValue() + ", " +args.get(1).getValue() + "]", source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
existingFilterChainMap.putAll(filterChainMap);
|
||||||
|
} else {
|
||||||
|
// Not already registered, so register it
|
||||||
BeanDefinitionBuilder fcpBldr = BeanDefinitionBuilder.rootBeanDefinition(FilterChainProxy.class);
|
BeanDefinitionBuilder fcpBldr = BeanDefinitionBuilder.rootBeanDefinition(FilterChainProxy.class);
|
||||||
fcpBldr.getRawBeanDefinition().setSource(source);
|
fcpBldr.getRawBeanDefinition().setSource(source);
|
||||||
// fcpBldr.addPropertyValue("stripQueryStringFromUrls", Boolean.valueOf(matcher instanceof AntUrlPathMatcher));
|
|
||||||
fcpBldr.addPropertyValue("filterChainMap", filterChainMap);
|
fcpBldr.addPropertyValue("filterChainMap", filterChainMap);
|
||||||
|
fcpBldr.addPropertyValue("filterChainValidator", new RootBeanDefinition(DefaultFilterChainValidator.class));
|
||||||
BeanDefinition fcpBean = fcpBldr.getBeanDefinition();
|
BeanDefinition fcpBean = fcpBldr.getBeanDefinition();
|
||||||
pc.registerBeanComponent(new BeanComponentDefinition(fcpBean, BeanIds.FILTER_CHAIN_PROXY));
|
pc.registerBeanComponent(new BeanComponentDefinition(fcpBean, BeanIds.FILTER_CHAIN_PROXY));
|
||||||
pc.getRegistry().registerAlias(BeanIds.FILTER_CHAIN_PROXY, BeanIds.SPRING_SECURITY_FILTER_CHAIN);
|
pc.getRegistry().registerAlias(BeanIds.FILTER_CHAIN_PROXY, BeanIds.SPRING_SECURITY_FILTER_CHAIN);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -257,6 +257,9 @@ protect-pointcut.attlist &=
|
|||||||
http =
|
http =
|
||||||
## Container element for HTTP security configuration
|
## Container element for HTTP security configuration
|
||||||
element http {http.attlist, (intercept-url+ & access-denied-handler? & form-login? & openid-login? & x509? & http-basic? & logout? & session-management & remember-me? & anonymous? & port-mappings & custom-filter* & request-cache?) }
|
element http {http.attlist, (intercept-url+ & access-denied-handler? & form-login? & openid-login? & x509? & http-basic? & logout? & session-management & remember-me? & anonymous? & port-mappings & custom-filter* & request-cache?) }
|
||||||
|
http.attlist &=
|
||||||
|
## The request URL pattern which will be mapped to the filter chain created by this <http> element. If omitted, the filter chain will match all requests.
|
||||||
|
attribute pattern {xsd:token}?
|
||||||
http.attlist &=
|
http.attlist &=
|
||||||
## Automatically registers a login form, BASIC authentication, anonymous authentication, logout services, remember-me and servlet-api-integration. If set to "true", all of these capabilities are added (although you can still customize the configuration of each by providing the respective element). If unspecified, defaults to "false".
|
## Automatically registers a login form, BASIC authentication, anonymous authentication, logout services, remember-me and servlet-api-integration. If set to "true", all of these capabilities are added (although you can still customize the configuration of each by providing the respective element). If unspecified, defaults to "false".
|
||||||
attribute auto-config {boolean}?
|
attribute auto-config {boolean}?
|
||||||
|
@ -687,6 +687,11 @@
|
|||||||
<xs:attributeGroup ref="security:http.attlist"/>
|
<xs:attributeGroup ref="security:http.attlist"/>
|
||||||
</xs:complexType></xs:element>
|
</xs:complexType></xs:element>
|
||||||
<xs:attributeGroup name="http.attlist">
|
<xs:attributeGroup name="http.attlist">
|
||||||
|
<xs:attribute name="pattern" type="xs:token">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>The request URL pattern which will be mapped to the filter chain created by this <http> element. If omitted, the filter chain will match all requests.</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
<xs:attribute name="auto-config" type="security:boolean">
|
<xs:attribute name="auto-config" type="security:boolean">
|
||||||
<xs:annotation>
|
<xs:annotation>
|
||||||
<xs:documentation>Automatically registers a login form, BASIC authentication, anonymous authentication, logout services, remember-me and servlet-api-integration. If set to "true", all of these capabilities are added (although you can still customize the configuration of each by providing the respective element). If unspecified, defaults to "false".</xs:documentation>
|
<xs:documentation>Automatically registers a login form, BASIC authentication, anonymous authentication, logout services, remember-me and servlet-api-integration. If set to "true", all of these capabilities are added (although you can still customize the configuration of each by providing the respective element). If unspecified, defaults to "false".</xs:documentation>
|
||||||
|
@ -82,16 +82,6 @@ class MiscHttpConfigTests extends AbstractHttpConfigTests {
|
|||||||
assert fsi.isObserveOncePerRequest()
|
assert fsi.isObserveOncePerRequest()
|
||||||
}
|
}
|
||||||
|
|
||||||
def duplicateElementCausesError() {
|
|
||||||
when: "Two http blocks are defined"
|
|
||||||
xml.http('auto-config': 'true')
|
|
||||||
xml.http('auto-config': 'true')
|
|
||||||
createAppContext()
|
|
||||||
|
|
||||||
then:
|
|
||||||
BeanDefinitionParsingException e = thrown();
|
|
||||||
}
|
|
||||||
|
|
||||||
def filterListShouldBeEmptyForPatternWithNoFilters() {
|
def filterListShouldBeEmptyForPatternWithNoFilters() {
|
||||||
httpAutoConfig() {
|
httpAutoConfig() {
|
||||||
interceptUrlNoFilters('/unprotected')
|
interceptUrlNoFilters('/unprotected')
|
||||||
|
@ -0,0 +1,47 @@
|
|||||||
|
package org.springframework.security.config.http
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.security.web.FilterChainProxy;
|
||||||
|
import org.springframework.security.config.BeanIds;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.parsing.BeanDefinitionParsingException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests scenarios with multiple <http> elements.
|
||||||
|
*
|
||||||
|
* @author Luke Taylor
|
||||||
|
*/
|
||||||
|
class MultiHttpBlockConfigTests extends AbstractHttpConfigTests {
|
||||||
|
|
||||||
|
def multipleHttpElementsAreSupported () {
|
||||||
|
when: "Two <http> elements are used"
|
||||||
|
xml.http(pattern: '/stateless/**', 'create-session': 'stateless') {
|
||||||
|
'http-basic'()
|
||||||
|
}
|
||||||
|
xml.http(pattern: '/stateful/**') {
|
||||||
|
'form-login'()
|
||||||
|
}
|
||||||
|
createAppContext()
|
||||||
|
FilterChainProxy fcp = appContext.getBean(BeanIds.FILTER_CHAIN_PROXY)
|
||||||
|
Map filterChains = fcp.getFilterChainMap();
|
||||||
|
|
||||||
|
then:
|
||||||
|
filterChains.size() == 2
|
||||||
|
(filterChains.keySet() as List)[0].pattern == '/stateless/**'
|
||||||
|
}
|
||||||
|
|
||||||
|
def duplicatePatternsAreRejected () {
|
||||||
|
when: "Two <http> elements are used"
|
||||||
|
xml.http(pattern: '/stateless/**', 'create-session': 'stateless') {
|
||||||
|
'http-basic'()
|
||||||
|
}
|
||||||
|
xml.http(pattern: '/stateless/**') {
|
||||||
|
'form-login'()
|
||||||
|
}
|
||||||
|
createAppContext()
|
||||||
|
then:
|
||||||
|
thrown(BeanDefinitionParsingException)
|
||||||
|
}
|
||||||
|
}
|
@ -80,6 +80,16 @@ public final class AntPathRequestMatcher implements RequestMatcher {
|
|||||||
return pattern;
|
return pattern;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (!(obj instanceof AntPathRequestMatcher)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
AntPathRequestMatcher other = (AntPathRequestMatcher)obj;
|
||||||
|
return this.pattern.equals(other.pattern) &&
|
||||||
|
this.httpMethod == other.httpMethod;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user