ソースを参照

Add Support Same Request Matchers Checking

Closes gh-15982
Max Batischev 8 ヶ月 前
コミット
e257af8854

+ 28 - 1
config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurityFilterChainValidator.java

@@ -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;
+			}
+		}
+	}
+
 }

+ 88 - 0
config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityFilterChainValidatorTests.java

@@ -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));
+	}
+
+}

+ 1 - 1
config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java

@@ -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) {