Add Support Same Request Matchers Checking

Closes gh-15982
This commit is contained in:
Max Batischev 2024-12-17 14:00:47 +03:00 committed by Josh Cummings
parent e9bdb5b96e
commit e257af8854
3 changed files with 117 additions and 2 deletions

View File

@ -21,11 +21,14 @@ import java.util.List;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.UnreachableFilterChainException;
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
/**
* A filter chain validator for filter chains built by {@link WebSecurity}
*
* @author Josh Cummings
* @author Max Batischev
* @since 6.5
*/
final class WebSecurityFilterChainValidator implements FilterChainProxy.FilterChainValidator {
@ -33,13 +36,18 @@ final class WebSecurityFilterChainValidator implements FilterChainProxy.FilterCh
@Override
public void validate(FilterChainProxy filterChainProxy) {
List<SecurityFilterChain> chains = filterChainProxy.getFilterChains();
checkForAnyRequestRequestMatcher(chains);
checkForDuplicateMatchers(chains);
}
private void checkForAnyRequestRequestMatcher(List<SecurityFilterChain> chains) {
DefaultSecurityFilterChain anyRequestFilterChain = null;
for (SecurityFilterChain chain : chains) {
if (anyRequestFilterChain != null) {
String message = "A filter chain that matches any request [" + anyRequestFilterChain
+ "] has already been configured, which means that this filter chain [" + chain
+ "] will never get invoked. Please use `HttpSecurity#securityMatcher` to ensure that there is only one filter chain configured for 'any request' and that the 'any request' filter chain is published last.";
throw new IllegalArgumentException(message);
throw new UnreachableFilterChainException(message, anyRequestFilterChain, chain);
}
if (chain instanceof DefaultSecurityFilterChain defaultChain) {
if (defaultChain.getRequestMatcher() instanceof AnyRequestMatcher) {
@ -49,4 +57,23 @@ final class WebSecurityFilterChainValidator implements FilterChainProxy.FilterCh
}
}
private void checkForDuplicateMatchers(List<SecurityFilterChain> chains) {
DefaultSecurityFilterChain filterChain = null;
for (SecurityFilterChain chain : chains) {
if (filterChain != null) {
if (chain instanceof DefaultSecurityFilterChain defaultChain) {
if (defaultChain.getRequestMatcher().equals(filterChain.getRequestMatcher())) {
throw new UnreachableFilterChainException(
"The FilterChainProxy contains two filter chains using the" + " matcher "
+ defaultChain.getRequestMatcher(),
filterChain, defaultChain);
}
}
}
if (chain instanceof DefaultSecurityFilterChain defaultChain) {
filterChain = defaultChain;
}
}
}
}

View File

@ -0,0 +1,88 @@
/*
* Copyright 2002-2024 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
*
* https://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.annotation.web.builders;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.UnreachableFilterChainException;
import org.springframework.security.web.access.ExceptionTranslationFilter;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link WebSecurityFilterChainValidator}
*
* @author Max Batischev
*/
@ExtendWith(MockitoExtension.class)
public class WebSecurityFilterChainValidatorTests {
private final WebSecurityFilterChainValidator validator = new WebSecurityFilterChainValidator();
@Mock
private AnonymousAuthenticationFilter authenticationFilter;
@Mock
private ExceptionTranslationFilter exceptionTranslationFilter;
@Mock
private FilterSecurityInterceptor authorizationInterceptor;
@Test
void validateWhenAnyRequestMatcherIsPresentThenUnreachableFilterChainException() {
SecurityFilterChain chain1 = new DefaultSecurityFilterChain(AntPathRequestMatcher.antMatcher("/api"),
this.authenticationFilter, this.exceptionTranslationFilter, this.authorizationInterceptor);
SecurityFilterChain chain2 = new DefaultSecurityFilterChain(AnyRequestMatcher.INSTANCE,
this.authenticationFilter, this.exceptionTranslationFilter, this.authorizationInterceptor);
List<SecurityFilterChain> chains = new ArrayList<>();
chains.add(chain2);
chains.add(chain1);
FilterChainProxy proxy = new FilterChainProxy(chains);
assertThatExceptionOfType(UnreachableFilterChainException.class)
.isThrownBy(() -> this.validator.validate(proxy));
}
@Test
void validateWhenSameRequestMatchersArePresentThenUnreachableFilterChainException() {
SecurityFilterChain chain1 = new DefaultSecurityFilterChain(AntPathRequestMatcher.antMatcher("/api"),
this.authenticationFilter, this.exceptionTranslationFilter, this.authorizationInterceptor);
SecurityFilterChain chain2 = new DefaultSecurityFilterChain(AntPathRequestMatcher.antMatcher("/api"),
this.authenticationFilter, this.exceptionTranslationFilter, this.authorizationInterceptor);
List<SecurityFilterChain> chains = new ArrayList<>();
chains.add(chain2);
chains.add(chain1);
FilterChainProxy proxy = new FilterChainProxy(chains);
assertThatExceptionOfType(UnreachableFilterChainException.class)
.isThrownBy(() -> this.validator.validate(proxy));
}
}

View File

@ -323,7 +323,7 @@ public class WebSecurityConfigurationTests {
assertThatExceptionOfType(BeanCreationException.class)
.isThrownBy(() -> this.spring.register(MultipleAnyRequestSecurityFilterChainConfig.class).autowire())
.havingRootCause()
.isExactlyInstanceOf(IllegalArgumentException.class);
.isInstanceOf(IllegalArgumentException.class);
}
private void assertAnotherUserPermission(WebInvocationPrivilegeEvaluator privilegeEvaluator) {