SEC-1657: Create SecurityFilterChain class for use in configuring FilterChinProxy. Encapsulates a RequestMatcher and List<Filter>.
This commit is contained in:
parent
614d8c0321
commit
37d0454fd7
|
@ -23,7 +23,6 @@ import org.springframework.security.web.firewall.HttpFirewall;
|
||||||
import org.springframework.security.web.util.AnyRequestMatcher;
|
import org.springframework.security.web.util.AnyRequestMatcher;
|
||||||
import org.springframework.security.web.util.RequestMatcher;
|
import org.springframework.security.web.util.RequestMatcher;
|
||||||
import org.springframework.security.web.util.UrlUtils;
|
import org.springframework.security.web.util.UrlUtils;
|
||||||
import org.springframework.util.Assert;
|
|
||||||
import org.springframework.web.filter.DelegatingFilterProxy;
|
import org.springframework.web.filter.DelegatingFilterProxy;
|
||||||
import org.springframework.web.filter.GenericFilterBean;
|
import org.springframework.web.filter.GenericFilterBean;
|
||||||
|
|
||||||
|
@ -49,21 +48,21 @@ import java.util.*;
|
||||||
*
|
*
|
||||||
* <h2>Configuration</h2>
|
* <h2>Configuration</h2>
|
||||||
* <p>
|
* <p>
|
||||||
* As of version 3.1, {@code FilterChainProxy} is configured using an ordered Map of {@link RequestMatcher} instances
|
* As of version 3.1, {@code FilterChainProxy} is configured using a list of {@link SecurityFilterChain} instances,
|
||||||
* to {@code List}s of {@code Filter}s. The Map instance will normally be created while parsing the namespace
|
* each of which contains a {@link RequestMatcher} and a list of filters which should be applied to matching requests.
|
||||||
* configuration, so doesn't have to be set explicitly. Instead the {@code <filter-chain-map>}
|
* Most applications will only contain a single filter chain, and if you are using the namespace, you don't have to
|
||||||
* element should be used within the bean declaration.
|
* set the chains explicitly. If you require finer-grained control, you can make use of the {@code <filter-chain>}
|
||||||
* This in turn should have a list of child {@code <filter-chain>} elements which each define a URI pattern and
|
* namespace element. This defines a URI pattern and the list of filters (as comma-separated bean names) which should be
|
||||||
* the list of filters (as comma-separated bean names) which should be applied to requests which match the pattern.
|
* applied to requests which match the pattern. An example configuration might look like this:
|
||||||
* The default pattern matching strategy is to use {@link org.springframework.security.web.util.AntPathRequestMatcher
|
|
||||||
* Ant-style paths}. An example configuration might look like this:
|
|
||||||
*
|
*
|
||||||
* <pre>
|
* <pre>
|
||||||
<bean id="myfilterChainProxy" class="org.springframework.security.util.FilterChjainProxy">
|
<bean id="myfilterChainProxy" class="org.springframework.security.util.FilterChainProxy">
|
||||||
<security:filter-chain-map request-matcher="ant">
|
<constructor-arg>
|
||||||
<security:filter-chain pattern="/do/not/filter*" filters="none"/>
|
<util:list>
|
||||||
<security:filter-chain pattern="/**" filters="filter1,filter2,filter3"/>
|
<security:filter-chain pattern="/do/not/filter*" filters="none"/>
|
||||||
</security:filter-chain-map>
|
<security:filter-chain pattern="/**" filters="filter1,filter2,filter3"/>
|
||||||
|
</util:list>
|
||||||
|
</constructor-arg>
|
||||||
</bean>
|
</bean>
|
||||||
* </pre>
|
* </pre>
|
||||||
*
|
*
|
||||||
|
@ -126,7 +125,7 @@ public class FilterChainProxy extends GenericFilterBean {
|
||||||
|
|
||||||
//~ Instance fields ================================================================================================
|
//~ Instance fields ================================================================================================
|
||||||
|
|
||||||
private Map<RequestMatcher, List<Filter>> filterChainMap;
|
private List<SecurityFilterChain> filterChains;
|
||||||
|
|
||||||
private FilterChainValidator filterChainValidator = new NullFilterChainValidator();
|
private FilterChainValidator filterChainValidator = new NullFilterChainValidator();
|
||||||
|
|
||||||
|
@ -134,9 +133,20 @@ public class FilterChainProxy extends GenericFilterBean {
|
||||||
|
|
||||||
//~ Methods ========================================================================================================
|
//~ Methods ========================================================================================================
|
||||||
|
|
||||||
|
public FilterChainProxy() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public FilterChainProxy(SecurityFilterChain chain) {
|
||||||
|
this(Arrays.asList(chain));
|
||||||
|
}
|
||||||
|
|
||||||
|
public FilterChainProxy(List<SecurityFilterChain> filterChains) {
|
||||||
|
this.filterChains = filterChains;
|
||||||
|
checkPathOrder();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterPropertiesSet() {
|
public void afterPropertiesSet() {
|
||||||
Assert.notNull(filterChainMap, "filterChainMap must be set");
|
|
||||||
filterChainValidator.validate(this);
|
filterChainValidator.validate(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,11 +182,9 @@ public class FilterChainProxy extends GenericFilterBean {
|
||||||
* @return an ordered array of Filters defining the filter chain
|
* @return an ordered array of Filters defining the filter chain
|
||||||
*/
|
*/
|
||||||
private List<Filter> getFilters(HttpServletRequest request) {
|
private List<Filter> getFilters(HttpServletRequest request) {
|
||||||
for (Map.Entry<RequestMatcher, List<Filter>> entry : filterChainMap.entrySet()) {
|
for (SecurityFilterChain chain : filterChains) {
|
||||||
RequestMatcher matcher = entry.getKey();
|
if (chain.matches(request)) {
|
||||||
|
return chain.getFilters();
|
||||||
if (matcher.matches(request)) {
|
|
||||||
return entry.getValue();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,34 +212,44 @@ public class FilterChainProxy extends GenericFilterBean {
|
||||||
* example.
|
* example.
|
||||||
*
|
*
|
||||||
* @param filterChainMap the map of path Strings to {@code List<Filter>}s.
|
* @param filterChainMap the map of path Strings to {@code List<Filter>}s.
|
||||||
|
* @deprecated Use the constructor which takes a {@code List<SecurityFilterChain>} instead.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
@Deprecated
|
||||||
public void setFilterChainMap(Map filterChainMap) {
|
public void setFilterChainMap(Map<RequestMatcher, List<Filter>> filterChainMap) {
|
||||||
checkContents(filterChainMap);
|
filterChains = new ArrayList<SecurityFilterChain>(filterChainMap.size());
|
||||||
this.filterChainMap = new LinkedHashMap<RequestMatcher, List<Filter>>(filterChainMap);
|
|
||||||
|
for (Map.Entry<RequestMatcher,List<Filter>> entry : filterChainMap.entrySet()) {
|
||||||
|
filterChains.add(new SecurityFilterChain(entry.getKey(), entry.getValue()));
|
||||||
|
}
|
||||||
|
|
||||||
checkPathOrder();
|
checkPathOrder();
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
/**
|
||||||
private void checkContents(Map filterChainMap) {
|
* Returns a copy of the underlying filter chain map. Modifications to the map contents
|
||||||
for (Object key : filterChainMap.keySet()) {
|
* will not affect the FilterChainProxy state.
|
||||||
Assert.isInstanceOf(RequestMatcher.class, key, "Path key must be a RequestMatcher but found " + key);
|
*
|
||||||
Object filters = filterChainMap.get(key);
|
* @return the map of path pattern Strings to filter chain lists (with ordering guaranteed).
|
||||||
Assert.isInstanceOf(List.class, filters, "Value must be a filter list");
|
*
|
||||||
// Check the contents
|
* @deprecated use the list of {@link SecurityFilterChain}s instead
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
public Map<RequestMatcher, List<Filter>> getFilterChainMap() {
|
||||||
|
LinkedHashMap<RequestMatcher, List<Filter>> map = new LinkedHashMap<RequestMatcher, List<Filter>>();
|
||||||
|
|
||||||
for (Object filter : ((List) filters)) {
|
for (SecurityFilterChain chain : filterChains) {
|
||||||
Assert.isInstanceOf(Filter.class, filter, "Objects in filter chain must be of type Filter. ");
|
map.put(chain.getRequestMatcher(), chain.getFilters());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkPathOrder() {
|
private void checkPathOrder() {
|
||||||
// Check that the universal pattern is listed at the end, if at all
|
// Check that the universal pattern is listed at the end, if at all
|
||||||
Iterator<RequestMatcher> matchers = filterChainMap.keySet().iterator();
|
Iterator<SecurityFilterChain> chains = filterChains.iterator();
|
||||||
|
|
||||||
while(matchers.hasNext()) {
|
while(chains.hasNext()) {
|
||||||
if ((matchers.next() instanceof AnyRequestMatcher && matchers.hasNext())) {
|
if ((chains.next().getRequestMatcher() instanceof AnyRequestMatcher && chains.hasNext())) {
|
||||||
throw new IllegalArgumentException("A universal match pattern ('/**') is defined " +
|
throw new IllegalArgumentException("A universal match pattern ('/**') is defined " +
|
||||||
" before other patterns in the filter chain, causing them to be ignored. Please check the " +
|
" 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");
|
"ordering in your <security:http> namespace or FilterChainProxy bean configuration");
|
||||||
|
@ -240,13 +258,11 @@ public class FilterChainProxy extends GenericFilterBean {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a copy of the underlying filter chain map. Modifications to the map contents
|
* @return the list of {@code SecurityFilterChain}s which will be matched against and
|
||||||
* will not affect the FilterChainProxy state - to change the map call {@code setFilterChainMap}.
|
* applied to incoming requests.
|
||||||
*
|
|
||||||
* @return the map of path pattern Strings to filter chain lists (with ordering guaranteed).
|
|
||||||
*/
|
*/
|
||||||
public Map<RequestMatcher, List<Filter>> getFilterChainMap() {
|
public List<SecurityFilterChain> getFilterChains() {
|
||||||
return new LinkedHashMap<RequestMatcher, List<Filter>>(filterChainMap);
|
return Collections.unmodifiableList(filterChains);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -273,7 +289,7 @@ public class FilterChainProxy extends GenericFilterBean {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
sb.append("FilterChainProxy[");
|
sb.append("FilterChainProxy[");
|
||||||
sb.append("Filter Chains: ");
|
sb.append("Filter Chains: ");
|
||||||
sb.append(filterChainMap);
|
sb.append(filterChains);
|
||||||
sb.append("]");
|
sb.append("]");
|
||||||
|
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
package org.springframework.security.web;
|
||||||
|
|
||||||
|
import org.springframework.security.web.util.RequestMatcher;
|
||||||
|
|
||||||
|
import javax.servlet.Filter;
|
||||||
|
import javax.servlet.FilterChain;
|
||||||
|
import javax.servlet.FilterConfig;
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.ServletRequest;
|
||||||
|
import javax.servlet.ServletResponse;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bean which defines a filter chain which is capable of being matched against an {@code HttpServletRequest}.
|
||||||
|
* in order to decide whether it applies to that request.
|
||||||
|
* <p>
|
||||||
|
* Used to configure a {@code FilterChainProxy}.
|
||||||
|
*
|
||||||
|
* @author Luke Taylor
|
||||||
|
*
|
||||||
|
* @since 3.1
|
||||||
|
*/
|
||||||
|
public final class SecurityFilterChain {
|
||||||
|
private final RequestMatcher requestMatcher;
|
||||||
|
private final List<Filter> filters;
|
||||||
|
|
||||||
|
public SecurityFilterChain(RequestMatcher requestMatcher, Filter... filters) {
|
||||||
|
this(requestMatcher, Arrays.asList(filters));
|
||||||
|
}
|
||||||
|
|
||||||
|
public SecurityFilterChain(RequestMatcher requestMatcher, List<Filter> filters) {
|
||||||
|
this.requestMatcher = requestMatcher;
|
||||||
|
this.filters = filters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RequestMatcher getRequestMatcher() {
|
||||||
|
return requestMatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Filter> getFilters() {
|
||||||
|
return filters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean matches(HttpServletRequest request) {
|
||||||
|
return requestMatcher.matches(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "[ " + requestMatcher + ", " + filters + "]";
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,8 +35,6 @@ public class FilterChainProxyTests {
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setup() throws Exception {
|
public void setup() throws Exception {
|
||||||
fcp = new FilterChainProxy();
|
|
||||||
fcp.setFilterChainValidator(mock(FilterChainProxy.FilterChainValidator.class));
|
|
||||||
matcher = mock(RequestMatcher.class);
|
matcher = mock(RequestMatcher.class);
|
||||||
filter = mock(Filter.class);
|
filter = mock(Filter.class);
|
||||||
doAnswer(new Answer() {
|
doAnswer(new Answer() {
|
||||||
|
@ -49,9 +47,8 @@ public class FilterChainProxyTests {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}).when(filter).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class), any(FilterChain.class));
|
}).when(filter).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class), any(FilterChain.class));
|
||||||
LinkedHashMap map = new LinkedHashMap();
|
fcp = new FilterChainProxy(new SecurityFilterChain(matcher, Arrays.asList(filter)));
|
||||||
map.put(matcher, Arrays.asList(filter));
|
fcp.setFilterChainValidator(mock(FilterChainProxy.FilterChainValidator.class));
|
||||||
fcp.setFilterChainMap(map);
|
|
||||||
request = new MockHttpServletRequest();
|
request = new MockHttpServletRequest();
|
||||||
request.setServletPath("/path");
|
request.setServletPath("/path");
|
||||||
response = new MockHttpServletResponse();
|
response = new MockHttpServletResponse();
|
||||||
|
@ -68,14 +65,23 @@ public class FilterChainProxyTests {
|
||||||
public void securityFilterChainIsNotInvokedIfMatchFails() throws Exception {
|
public void securityFilterChainIsNotInvokedIfMatchFails() throws Exception {
|
||||||
when(matcher.matches(any(HttpServletRequest.class))).thenReturn(false);
|
when(matcher.matches(any(HttpServletRequest.class))).thenReturn(false);
|
||||||
fcp.doFilter(request, response, chain);
|
fcp.doFilter(request, response, chain);
|
||||||
assertEquals(1, fcp.getFilterChainMap().size());
|
assertEquals(1, fcp.getFilterChains().size());
|
||||||
assertSame(filter, fcp.getFilterChainMap().get(matcher).get(0));
|
assertSame(filter, fcp.getFilterChains().get(0).getFilters().get(0));
|
||||||
|
|
||||||
verifyZeroInteractions(filter);
|
verifyZeroInteractions(filter);
|
||||||
// The actual filter chain should be invoked though
|
// The actual filter chain should be invoked though
|
||||||
verify(chain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
verify(chain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Deprecated
|
||||||
|
public void filterChainMapIsCorrect() throws Exception {
|
||||||
|
fcp.setFilterChainMap(fcp.getFilterChainMap());
|
||||||
|
Map<RequestMatcher, List<Filter>> filterChainMap = fcp.getFilterChainMap();
|
||||||
|
assertEquals(1, filterChainMap.size());
|
||||||
|
assertSame(filter, filterChainMap.get(matcher).get(0));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void originalChainIsInvokedAfterSecurityChainIfMatchSucceeds() throws Exception {
|
public void originalChainIsInvokedAfterSecurityChainIfMatchSucceeds() throws Exception {
|
||||||
when(matcher.matches(any(HttpServletRequest.class))).thenReturn(true);
|
when(matcher.matches(any(HttpServletRequest.class))).thenReturn(true);
|
||||||
|
@ -87,9 +93,8 @@ public class FilterChainProxyTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void originalFilterChainIsInvokedIfMatchingSecurityChainIsEmpty() throws Exception {
|
public void originalFilterChainIsInvokedIfMatchingSecurityChainIsEmpty() throws Exception {
|
||||||
LinkedHashMap map = new LinkedHashMap();
|
List<Filter> noFilters = Collections.emptyList();
|
||||||
map.put(matcher, Collections.emptyList());
|
fcp = new FilterChainProxy(new SecurityFilterChain(matcher, noFilters));
|
||||||
fcp.setFilterChainMap(map);
|
|
||||||
|
|
||||||
when(matcher.matches(any(HttpServletRequest.class))).thenReturn(true);
|
when(matcher.matches(any(HttpServletRequest.class))).thenReturn(true);
|
||||||
fcp.doFilter(request, response, chain);
|
fcp.doFilter(request, response, chain);
|
||||||
|
@ -132,10 +137,7 @@ public class FilterChainProxyTests {
|
||||||
@Test
|
@Test
|
||||||
public void bothWrappersAreResetWithNestedFcps() throws Exception {
|
public void bothWrappersAreResetWithNestedFcps() throws Exception {
|
||||||
HttpFirewall fw = mock(HttpFirewall.class);
|
HttpFirewall fw = mock(HttpFirewall.class);
|
||||||
FilterChainProxy firstFcp = new FilterChainProxy();
|
FilterChainProxy firstFcp = new FilterChainProxy(new SecurityFilterChain(matcher, fcp));
|
||||||
LinkedHashMap fcm = new LinkedHashMap();
|
|
||||||
fcm.put(matcher, Arrays.asList(fcp));
|
|
||||||
firstFcp.setFilterChainMap(fcm);
|
|
||||||
firstFcp.setFirewall(fw);
|
firstFcp.setFirewall(fw);
|
||||||
fcp.setFirewall(fw);
|
fcp.setFirewall(fw);
|
||||||
FirewalledRequest firstFwr = mock(FirewalledRequest.class, "firstFwr");
|
FirewalledRequest firstFwr = mock(FirewalledRequest.class, "firstFwr");
|
||||||
|
@ -153,4 +155,4 @@ public class FilterChainProxyTests {
|
||||||
verify(firstFwr).reset();
|
verify(firstFwr).reset();
|
||||||
verify(fwr).reset();
|
verify(fwr).reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue