Ver Fonte

Add forServletPattern

Closes gh-13562
Josh Cummings há 1 ano atrás
pai
commit
762319b6be
17 ficheiros alterados com 2190 adições e 76 exclusões
  1. 52 0
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractRequestMatcherBuilderRegistry.java
  2. 59 0
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/AntPathRequestMatcherBuilder.java
  3. 241 33
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java
  4. 103 0
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/DispatcherServletDelegatingRequestMatcherBuilder.java
  5. 76 0
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/MvcRequestMatcherBuilder.java
  6. 106 0
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestMatcherBuilder.java
  7. 215 0
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestMatcherBuilders.java
  8. 43 0
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletPatternRequestMatcher.java
  9. 152 0
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletRegistrationCollection.java
  10. 7 0
      config/src/test/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryNoMvcTests.java
  11. 0 6
      config/src/test/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryTests.java
  12. 349 0
      config/src/test/java/org/springframework/security/config/annotation/web/configurers/AbstractRequestMatcherBuilderRegistryTests.java
  13. 362 5
      config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java
  14. 198 0
      config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestMatcherBuildersTests.java
  15. 63 0
      config/src/test/java/org/springframework/security/config/annotation/web/configurers/ServletPatternRequestMatcherTests.java
  16. 46 0
      config/src/test/java/org/springframework/security/config/annotation/web/configurers/TestMockHttpServletMappings.java
  17. 118 32
      docs/modules/ROOT/pages/servlet/authorization/authorize-http-requests.adoc

+ 52 - 0
config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractRequestMatcherBuilderRegistry.java

@@ -0,0 +1,52 @@
+/*
+ * Copyright 2002-2023 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.configurers;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+
+abstract class AbstractRequestMatcherBuilderRegistry<C> extends AbstractRequestMatcherRegistry<C> {
+
+	private final RequestMatcherBuilder builder;
+
+	AbstractRequestMatcherBuilderRegistry(ApplicationContext context) {
+		this(context, RequestMatcherBuilders.createDefault(context));
+	}
+
+	AbstractRequestMatcherBuilderRegistry(ApplicationContext context, RequestMatcherBuilder builder) {
+		setApplicationContext(context);
+		this.builder = builder;
+	}
+
+	@Override
+	public final C requestMatchers(String... patterns) {
+		return requestMatchers(null, patterns);
+	}
+
+	@Override
+	public final C requestMatchers(HttpMethod method, String... patterns) {
+		return requestMatchers(this.builder.matchers(method, patterns).toArray(RequestMatcher[]::new));
+	}
+
+	@Override
+	public final C requestMatchers(HttpMethod method) {
+		return requestMatchers(method, "/**");
+	}
+
+}

+ 59 - 0
config/src/main/java/org/springframework/security/config/annotation/web/configurers/AntPathRequestMatcherBuilder.java

@@ -0,0 +1,59 @@
+/*
+ * Copyright 2002-2023 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.configurers;
+
+import org.springframework.http.HttpMethod;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+
+final class AntPathRequestMatcherBuilder implements RequestMatcherBuilder {
+
+	private final String servletPath;
+
+	private AntPathRequestMatcherBuilder(String servletPath) {
+		this.servletPath = servletPath;
+	}
+
+	static AntPathRequestMatcherBuilder absolute() {
+		return new AntPathRequestMatcherBuilder(null);
+	}
+
+	static AntPathRequestMatcherBuilder relativeTo(String path) {
+		return new AntPathRequestMatcherBuilder(path);
+	}
+
+	@Override
+	public AntPathRequestMatcher matcher(String pattern) {
+		return matcher((String) null, pattern);
+	}
+
+	@Override
+	public AntPathRequestMatcher matcher(HttpMethod method, String pattern) {
+		return matcher((method != null) ? method.name() : null, pattern);
+	}
+
+	private AntPathRequestMatcher matcher(String method, String pattern) {
+		return new AntPathRequestMatcher(prependServletPath(pattern), method);
+	}
+
+	private String prependServletPath(String pattern) {
+		if (this.servletPath == null) {
+			return pattern;
+		}
+		return this.servletPath + pattern;
+	}
+
+}

+ 241 - 33
config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java

@@ -16,10 +16,14 @@
 
 package org.springframework.security.config.annotation.web.configurers;
 
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
 import java.util.function.Supplier;
 
 import io.micrometer.observation.ObservationRegistry;
+import jakarta.servlet.http.HttpServletMapping;
 import jakarta.servlet.http.HttpServletRequest;
 
 import org.springframework.context.ApplicationContext;
@@ -32,17 +36,22 @@ import org.springframework.security.authorization.AuthorizationEventPublisher;
 import org.springframework.security.authorization.AuthorizationManager;
 import org.springframework.security.authorization.ObservationAuthorizationManager;
 import org.springframework.security.authorization.SpringAuthorizationEventPublisher;
+import org.springframework.security.config.Customizer;
 import org.springframework.security.config.annotation.ObjectPostProcessor;
 import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry;
 import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.core.GrantedAuthorityDefaults;
 import org.springframework.security.web.access.intercept.AuthorizationFilter;
 import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
 import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager;
+import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcherEntry;
 import org.springframework.util.Assert;
 import org.springframework.util.function.SingletonSupplier;
+import org.springframework.web.servlet.DispatcherServlet;
 
 /**
  * Adds a URL based authorization using {@link AuthorizationManager}.
@@ -137,41 +146,62 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
 	 * @author Evgeniy Cheban
 	 */
 	public final class AuthorizationManagerRequestMatcherRegistry
-			extends AbstractRequestMatcherRegistry<AuthorizedUrl> {
+			extends AbstractRequestMatcherBuilderRegistry<AuthorizedUrl<AuthorizationManagerRequestMatcherRegistry>> {
 
 		private final RequestMatcherDelegatingAuthorizationManager.Builder managerBuilder = RequestMatcherDelegatingAuthorizationManager
 			.builder();
 
-		private List<RequestMatcher> unmappedMatchers;
+		List<RequestMatcher> unmappedMatchers;
 
 		private int mappingCount;
 
 		private boolean shouldFilterAllDispatcherTypes = true;
 
-		private AuthorizationManagerRequestMatcherRegistry(ApplicationContext context) {
-			setApplicationContext(context);
+		private final Map<String, AuthorizationManagerServletRequestMatcherRegistry> servletPattern = new LinkedHashMap<>();
+
+		AuthorizationManagerRequestMatcherRegistry(ApplicationContext context) {
+			super(context);
 		}
 
 		private void addMapping(RequestMatcher matcher, AuthorizationManager<RequestAuthorizationContext> manager) {
+			Assert.isTrue(this.servletPattern.isEmpty(),
+					"Since you have used forServletPattern, all request matchers must be configured using forServletPattern; alternatively, you can use requestMatchers(RequestMatcher) for all requests.");
 			this.unmappedMatchers = null;
 			this.managerBuilder.add(matcher, manager);
 			this.mappingCount++;
 		}
 
 		private void addFirst(RequestMatcher matcher, AuthorizationManager<RequestAuthorizationContext> manager) {
+			Assert.isTrue(this.servletPattern.isEmpty(),
+					"Since you have used forServletPattern, all request matchers must be configured using forServletPattern; alternatively, you can use requestMatchers(RequestMatcher) for all requests.");
 			this.unmappedMatchers = null;
 			this.managerBuilder.mappings((m) -> m.add(0, new RequestMatcherEntry<>(matcher, manager)));
 			this.mappingCount++;
 		}
 
-		private AuthorizationManager<HttpServletRequest> createAuthorizationManager() {
+		private AuthorizationManager<HttpServletRequest> servletAuthorizationManager() {
+			for (Map.Entry<String, AuthorizationManagerServletRequestMatcherRegistry> entry : this.servletPattern
+				.entrySet()) {
+				AuthorizationManagerServletRequestMatcherRegistry registry = entry.getValue();
+				this.managerBuilder.add(new ServletPatternRequestMatcher(entry.getKey()),
+						registry.authorizationManager());
+			}
+			return postProcess(this.managerBuilder.build());
+		}
+
+		private AuthorizationManager<HttpServletRequest> authorizationManager() {
 			Assert.state(this.unmappedMatchers == null,
 					() -> "An incomplete mapping was found for " + this.unmappedMatchers
 							+ ". Try completing it with something like requestUrls().<something>.hasRole('USER')");
 			Assert.state(this.mappingCount > 0,
 					"At least one mapping is required (for example, authorizeHttpRequests().anyRequest().authenticated())");
+			return postProcess(this.managerBuilder.build());
+		}
+
+		private AuthorizationManager<HttpServletRequest> createAuthorizationManager() {
+			AuthorizationManager<HttpServletRequest> manager = (this.servletPattern.isEmpty()) ? authorizationManager()
+					: servletAuthorizationManager();
 			ObservationRegistry registry = getObservationRegistry();
-			RequestMatcherDelegatingAuthorizationManager manager = postProcess(this.managerBuilder.build());
 			if (registry.isNoop()) {
 				return manager;
 			}
@@ -179,9 +209,77 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
 		}
 
 		@Override
-		protected AuthorizedUrl chainRequestMatchers(List<RequestMatcher> requestMatchers) {
+		protected AuthorizedUrl<AuthorizationManagerRequestMatcherRegistry> chainRequestMatchers(
+				List<RequestMatcher> requestMatchers) {
 			this.unmappedMatchers = requestMatchers;
-			return new AuthorizedUrl(requestMatchers);
+			return new AuthorizedUrl<>(
+					(manager) -> AuthorizeHttpRequestsConfigurer.this.addMapping(requestMatchers, manager));
+		}
+
+		/**
+		 * Begin registering {@link RequestMatcher}s based on the type of the servlet
+		 * mapped to {@code pattern}. Each registered request matcher will additionally
+		 * check {@link HttpServletMapping#getPattern} against the provided
+		 * {@code pattern}.
+		 *
+		 * <p>
+		 * If the corresponding servlet is of type {@link DispatcherServlet}, then use a
+		 * {@link AuthorizationManagerServletRequestMatcherRegistry} that registers
+		 * {@link MvcRequestMatcher}s.
+		 *
+		 * <p>
+		 * Otherwise, use a configurer that registers {@link AntPathRequestMatcher}s.
+		 *
+		 * <p>
+		 * When doing a path-based pattern, like `/path/*`, registered URIs should leave
+		 * out the matching path. For example, if the target URI is `/path/resource/3`,
+		 * then the configuration should look like this: <code>
+		 *	.forServletPattern("/path/*", (path) -> path
+		 *      .requestMatchers("/resource/3").hasAuthority(...)
+		 *  )
+		 * </code>
+		 *
+		 * <p>
+		 * Or, if the pattern is `/path/subpath/*`, and the URI is
+		 * `/path/subpath/resource/3`, then the configuration should look like this:
+		 * <code>
+		 *	.forServletPattern("/path/subpath/*", (path) -> path
+		 *      .requestMatchers("/resource/3").hasAuthority(...)
+		 *  )
+		 * </code>
+		 *
+		 * <p>
+		 * For all other patterns, please supply the URI in absolute terms. For example,
+		 * if the target URI is `/js/**` and it matches to the default servlet, then the
+		 * configuration should look like this: <code>
+		 * 	.forServletPattern("/", (root) -> root
+		 * 	    .requestMatchers("/js/**").hasAuthority(...)
+		 * 	)
+		 * </code>
+		 *
+		 * <p>
+		 * Or, if the target URI is `/views/**`, and it matches to a `*.jsp` extension
+		 * servlet, then the configuration should look like this: <code>
+		 * 	.forServletPattern("*.jsp", (jsp) -> jsp
+		 * 	    .requestMatchers("/views/**").hasAuthority(...)
+		 * 	)
+		 * </code>
+		 * @param customizer a customizer that uses a
+		 * {@link AuthorizationManagerServletRequestMatcherRegistry} for URIs mapped to
+		 * the provided servlet
+		 * @return an {@link AuthorizationManagerServletRequestMatcherRegistry} for
+		 * further configurations
+		 * @since 6.2
+		 */
+		public AuthorizationManagerRequestMatcherRegistry forServletPattern(String pattern,
+				Customizer<AuthorizationManagerServletRequestMatcherRegistry> customizer) {
+			ApplicationContext context = getApplicationContext();
+			RequestMatcherBuilder builder = RequestMatcherBuilders.createForServletPattern(context, pattern);
+			AuthorizationManagerServletRequestMatcherRegistry registry = new AuthorizationManagerServletRequestMatcherRegistry(
+					builder);
+			customizer.customize(registry);
+			this.servletPattern.put(pattern, registry);
+			return this;
 		}
 
 		/**
@@ -237,6 +335,125 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
 			return AuthorizeHttpRequestsConfigurer.this.and();
 		}
 
+		/**
+		 * A decorator class for registering {@link RequestMatcher} instances based on the
+		 * type of servlet. If the servlet is {@link DispatcherServlet}, then it will use
+		 * a {@link MvcRequestMatcher}; otherwise, it will use a
+		 * {@link AntPathRequestMatcher}.
+		 *
+		 * <p>
+		 * This class is designed primarily for use with the {@link HttpSecurity} DSL. For
+		 * that reason, please use {@link HttpSecurity#authorizeHttpRequests} instead as
+		 * it exposes this class fluently alongside related DSL configurations.
+		 *
+		 * <p>
+		 * NOTE: In many cases, which kind of request matcher is needed is apparent by the
+		 * servlet configuration, and so you should generally use the methods found in
+		 * {@link AbstractRequestMatcherRegistry} instead of this these. Use this class
+		 * when you want or need to indicate which request matcher URIs belong to which
+		 * servlet.
+		 *
+		 * <p>
+		 * In all cases, though, you may arrange your request matchers by servlet pattern
+		 * with the {@link AuthorizationManagerRequestMatcherRegistry#forServletPattern}
+		 * method in the {@link HttpSecurity#authorizeHttpRequests} DSL.
+		 *
+		 * <p>
+		 * Consider, for example, the circumstance where you have Spring MVC configured
+		 * and also Spring Boot H2 Console. Spring MVC registers a servlet of type
+		 * {@link DispatcherServlet} as the default servlet and Spring Boot registers a
+		 * servlet of its own as well at `/h2-console/*`.
+		 *
+		 * <p>
+		 * Such might have a configuration like this in Spring Security: <code>
+		 * 	http
+		 * 		.authorizeHttpRequests((authorize) -> authorize
+		 * 			.requestMatchers("/js/**", "/css/**").permitAll()
+		 * 			.requestMatchers("/my/controller/**").hasAuthority("CONTROLLER")
+		 * 			.requestMatchers("/h2-console/**").hasAuthority("H2")
+		 * 		)
+		 * 		// ...
+		 * </code>
+		 *
+		 * <p>
+		 * Spring Security by default addresses the above configuration on its own.
+		 *
+		 * <p>
+		 * However, consider the same situation, but where {@link DispatcherServlet} is
+		 * mapped to a path like `/mvc/*`. In this case, the above configuration is
+		 * ambiguous, and you should use this class to clarify the rest of each MVC URI
+		 * like so: <code>
+		 * 	http
+		 * 		.authorizeHttpRequests((authorize) -> authorize
+		 * 			.forServletPattern("/", (root) -> root
+		 * 				.requestMatchers("/js/**", "/css/**").permitAll()
+		 * 			)
+		 * 			.forServletPattern("/mvc/*", (mvc) -> mvc
+		 * 				.requestMatchers("/my/controller/**").hasAuthority("CONTROLLER")
+		 * 			)
+		 * 			.forServletPattern("/h2-console/*", (h2) -> h2
+		 * 				.anyRequest().hasAuthority("OTHER")
+		 * 			)
+		 * 		)
+		 * 		// ...
+		 * </code>
+		 *
+		 * <p>
+		 * In the above configuration, it's now clear to Spring Security that the
+		 * following matchers map to these corresponding URIs:
+		 *
+		 * <ul>
+		 * <li>&lt;default&gt; + <strong>`/js/**`</strong> ==> `/js/**`</li>
+		 * <li>&lt;default&gt; + <strong>`/css/**`</strong> ==> `/css/**`</li>
+		 * <li>`/mvc` + <strong>`/my/controller/**`</strong> ==>
+		 * `/mvc/my/controller/**`</li>
+		 * <li>`/h2-console` + <strong>&lt;any request&gt;</strong> ==>
+		 * `/h2-console/**`</li>
+		 * </ul>
+		 *
+		 * @author Josh Cummings
+		 * @since 6.2
+		 * @see AbstractRequestMatcherRegistry
+		 * @see AuthorizeHttpRequestsConfigurer
+		 */
+		public final class AuthorizationManagerServletRequestMatcherRegistry extends
+				AbstractRequestMatcherBuilderRegistry<AuthorizedUrl<AuthorizationManagerServletRequestMatcherRegistry>> {
+
+			private final RequestMatcherDelegatingAuthorizationManager.Builder managerBuilder = RequestMatcherDelegatingAuthorizationManager
+				.builder();
+
+			private List<RequestMatcher> unmappedMatchers;
+
+			AuthorizationManagerServletRequestMatcherRegistry(RequestMatcherBuilder builder) {
+				super(AuthorizationManagerRequestMatcherRegistry.this.getApplicationContext(), builder);
+			}
+
+			AuthorizationManager<RequestAuthorizationContext> authorizationManager() {
+				Assert.state(this.unmappedMatchers == null,
+						() -> "An incomplete mapping was found for " + this.unmappedMatchers
+								+ ". Try completing it with something like requestUrls().<something>.hasRole('USER')");
+				AuthorizationManager<HttpServletRequest> request = this.managerBuilder.build();
+				return (authentication, context) -> request.check(authentication, context.getRequest());
+			}
+
+			@Override
+			protected AuthorizedUrl<AuthorizationManagerServletRequestMatcherRegistry> chainRequestMatchers(
+					List<RequestMatcher> requestMatchers) {
+				this.unmappedMatchers = requestMatchers;
+				return new AuthorizedUrl<>((manager) -> addMapping(requestMatchers, manager));
+			}
+
+			private AuthorizationManagerServletRequestMatcherRegistry addMapping(List<RequestMatcher> matchers,
+					AuthorizationManager<RequestAuthorizationContext> manager) {
+				this.unmappedMatchers = null;
+				for (RequestMatcher matcher : matchers) {
+					this.managerBuilder.add(matcher, manager);
+				}
+				return this;
+			}
+
+		}
+
 	}
 
 	/**
@@ -245,20 +462,12 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
 	 *
 	 * @author Evgeniy Cheban
 	 */
-	public class AuthorizedUrl {
-
-		private final List<? extends RequestMatcher> matchers;
+	public class AuthorizedUrl<R> {
 
-		/**
-		 * Creates an instance.
-		 * @param matchers the {@link RequestMatcher} instances to map
-		 */
-		AuthorizedUrl(List<? extends RequestMatcher> matchers) {
-			this.matchers = matchers;
-		}
+		private final Function<AuthorizationManager<RequestAuthorizationContext>, R> registrar;
 
-		protected List<? extends RequestMatcher> getMatchers() {
-			return this.matchers;
+		AuthorizedUrl(Function<AuthorizationManager<RequestAuthorizationContext>, R> registrar) {
+			this.registrar = registrar;
 		}
 
 		/**
@@ -266,7 +475,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
 		 * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further
 		 * customizations
 		 */
-		public AuthorizationManagerRequestMatcherRegistry permitAll() {
+		public R permitAll() {
 			return access(permitAllAuthorizationManager);
 		}
 
@@ -275,7 +484,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
 		 * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further
 		 * customizations
 		 */
-		public AuthorizationManagerRequestMatcherRegistry denyAll() {
+		public R denyAll() {
 			return access((a, o) -> new AuthorizationDecision(false));
 		}
 
@@ -286,7 +495,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
 		 * @return {@link AuthorizationManagerRequestMatcherRegistry} for further
 		 * customizations
 		 */
-		public AuthorizationManagerRequestMatcherRegistry hasRole(String role) {
+		public R hasRole(String role) {
 			return access(withRoleHierarchy(AuthorityAuthorizationManager
 				.hasAnyRole(AuthorizeHttpRequestsConfigurer.this.rolePrefix, new String[] { role })));
 		}
@@ -299,7 +508,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
 		 * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further
 		 * customizations
 		 */
-		public AuthorizationManagerRequestMatcherRegistry hasAnyRole(String... roles) {
+		public R hasAnyRole(String... roles) {
 			return access(withRoleHierarchy(
 					AuthorityAuthorizationManager.hasAnyRole(AuthorizeHttpRequestsConfigurer.this.rolePrefix, roles)));
 		}
@@ -310,7 +519,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
 		 * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further
 		 * customizations
 		 */
-		public AuthorizationManagerRequestMatcherRegistry hasAuthority(String authority) {
+		public R hasAuthority(String authority) {
 			return access(withRoleHierarchy(AuthorityAuthorizationManager.hasAuthority(authority)));
 		}
 
@@ -321,7 +530,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
 		 * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further
 		 * customizations
 		 */
-		public AuthorizationManagerRequestMatcherRegistry hasAnyAuthority(String... authorities) {
+		public R hasAnyAuthority(String... authorities) {
 			return access(withRoleHierarchy(AuthorityAuthorizationManager.hasAnyAuthority(authorities)));
 		}
 
@@ -336,7 +545,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
 		 * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further
 		 * customizations
 		 */
-		public AuthorizationManagerRequestMatcherRegistry authenticated() {
+		public R authenticated() {
 			return access(AuthenticatedAuthorizationManager.authenticated());
 		}
 
@@ -348,7 +557,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
 		 * @since 5.8
 		 * @see RememberMeConfigurer
 		 */
-		public AuthorizationManagerRequestMatcherRegistry fullyAuthenticated() {
+		public R fullyAuthenticated() {
 			return access(AuthenticatedAuthorizationManager.fullyAuthenticated());
 		}
 
@@ -359,7 +568,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
 		 * @since 5.8
 		 * @see RememberMeConfigurer
 		 */
-		public AuthorizationManagerRequestMatcherRegistry rememberMe() {
+		public R rememberMe() {
 			return access(AuthenticatedAuthorizationManager.rememberMe());
 		}
 
@@ -369,7 +578,7 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
 		 * customization
 		 * @since 5.8
 		 */
-		public AuthorizationManagerRequestMatcherRegistry anonymous() {
+		public R anonymous() {
 			return access(AuthenticatedAuthorizationManager.anonymous());
 		}
 
@@ -379,10 +588,9 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
 		 * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further
 		 * customizations
 		 */
-		public AuthorizationManagerRequestMatcherRegistry access(
-				AuthorizationManager<RequestAuthorizationContext> manager) {
+		public R access(AuthorizationManager<RequestAuthorizationContext> manager) {
 			Assert.notNull(manager, "manager cannot be null");
-			return AuthorizeHttpRequestsConfigurer.this.addMapping(this.matchers, manager);
+			return this.registrar.apply(manager);
 		}
 
 	}

+ 103 - 0
config/src/main/java/org/springframework/security/config/annotation/web/configurers/DispatcherServletDelegatingRequestMatcherBuilder.java

@@ -0,0 +1,103 @@
+/*
+ * Copyright 2002-2023 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.configurers;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+import org.springframework.http.HttpMethod;
+import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.Assert;
+
+final class DispatcherServletDelegatingRequestMatcherBuilder implements RequestMatcherBuilder {
+
+	final MvcRequestMatcherBuilder mvc;
+
+	final AntPathRequestMatcherBuilder ant;
+
+	final ServletRegistrationCollection registrations;
+
+	DispatcherServletDelegatingRequestMatcherBuilder(MvcRequestMatcherBuilder mvc, AntPathRequestMatcherBuilder ant,
+			ServletRegistrationCollection registrations) {
+		this.mvc = mvc;
+		this.ant = ant;
+		this.registrations = registrations;
+	}
+
+	@Override
+	public RequestMatcher matcher(String pattern) {
+		MvcRequestMatcher mvc = this.mvc.matcher(pattern);
+		AntPathRequestMatcher ant = this.ant.matcher(pattern);
+		return new DispatcherServletDelegatingRequestMatcher(mvc, ant, this.registrations);
+	}
+
+	@Override
+	public RequestMatcher matcher(HttpMethod method, String pattern) {
+		MvcRequestMatcher mvc = this.mvc.matcher(method, pattern);
+		AntPathRequestMatcher ant = this.ant.matcher(method, pattern);
+		return new DispatcherServletDelegatingRequestMatcher(mvc, ant, this.registrations);
+	}
+
+	static final class DispatcherServletDelegatingRequestMatcher implements RequestMatcher {
+
+		private final MvcRequestMatcher mvc;
+
+		private final AntPathRequestMatcher ant;
+
+		private final ServletRegistrationCollection registrations;
+
+		private DispatcherServletDelegatingRequestMatcher(MvcRequestMatcher mvc, AntPathRequestMatcher ant,
+				ServletRegistrationCollection registrations) {
+			this.mvc = mvc;
+			this.ant = ant;
+			this.registrations = registrations;
+		}
+
+		@Override
+		public boolean matches(HttpServletRequest request) {
+			String name = request.getHttpServletMapping().getServletName();
+			ServletRegistrationCollection.Registration registration = this.registrations.registrationByName(name);
+			Assert.notNull(registration,
+					String.format("Could not find %s in servlet configuration %s", name, this.registrations));
+			if (registration.isDispatcherServlet()) {
+				return this.mvc.matches(request);
+			}
+			return this.ant.matches(request);
+		}
+
+		@Override
+		public MatchResult matcher(HttpServletRequest request) {
+			String name = request.getHttpServletMapping().getServletName();
+			ServletRegistrationCollection.Registration registration = this.registrations.registrationByName(name);
+			Assert.notNull(registration,
+					String.format("Could not find %s in servlet configuration %s", name, this.registrations));
+			if (registration.isDispatcherServlet()) {
+				return this.mvc.matcher(request);
+			}
+			return this.ant.matcher(request);
+		}
+
+		@Override
+		public String toString() {
+			return String.format("DispatcherServlet [mvc=[%s], ant=[%s], servlet=[%s]]", this.mvc, this.ant,
+					this.registrations);
+		}
+
+	}
+
+}

+ 76 - 0
config/src/main/java/org/springframework/security/config/annotation/web/configurers/MvcRequestMatcherBuilder.java

@@ -0,0 +1,76 @@
+/*
+ * Copyright 2002-2023 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.configurers;
+
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.config.annotation.ObjectPostProcessor;
+import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
+import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
+
+final class MvcRequestMatcherBuilder implements RequestMatcherBuilder {
+
+	private static final String HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME = "mvcHandlerMappingIntrospector";
+
+	private final HandlerMappingIntrospector introspector;
+
+	private final ObjectPostProcessor<Object> objectPostProcessor;
+
+	private final String servletPath;
+
+	private MvcRequestMatcherBuilder(ApplicationContext context, String servletPath) {
+		if (!context.containsBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)) {
+			throw new NoSuchBeanDefinitionException("A Bean named " + HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME
+					+ " of type " + HandlerMappingIntrospector.class.getName()
+					+ " is required to use MvcRequestMatcher. Please ensure Spring Security & Spring MVC are configured in a shared ApplicationContext.");
+		}
+		this.introspector = context.getBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME, HandlerMappingIntrospector.class);
+		this.objectPostProcessor = context.getBean(ObjectPostProcessor.class);
+		this.servletPath = servletPath;
+	}
+
+	static MvcRequestMatcherBuilder absolute(ApplicationContext context) {
+		return new MvcRequestMatcherBuilder(context, null);
+	}
+
+	static MvcRequestMatcherBuilder relativeTo(ApplicationContext context, String path) {
+		return new MvcRequestMatcherBuilder(context, path);
+	}
+
+	@Override
+	public MvcRequestMatcher matcher(String pattern) {
+		MvcRequestMatcher matcher = new MvcRequestMatcher(this.introspector, pattern);
+		this.objectPostProcessor.postProcess(matcher);
+		if (this.servletPath != null) {
+			matcher.setServletPath(this.servletPath);
+		}
+		return matcher;
+	}
+
+	@Override
+	public MvcRequestMatcher matcher(HttpMethod method, String pattern) {
+		MvcRequestMatcher matcher = new MvcRequestMatcher(this.introspector, pattern);
+		this.objectPostProcessor.postProcess(matcher);
+		matcher.setMethod(method);
+		if (this.servletPath != null) {
+			matcher.setServletPath(this.servletPath);
+		}
+		return matcher;
+	}
+
+}

+ 106 - 0
config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestMatcherBuilder.java

@@ -0,0 +1,106 @@
+/*
+ * Copyright 2002-2023 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.configurers;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.http.HttpMethod;
+import org.springframework.security.web.util.matcher.AnyRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+
+/**
+ * An interface that abstracts how matchers are created
+ *
+ * @author Josh Cummings
+ * @since 6.2
+ */
+interface RequestMatcherBuilder {
+
+	/**
+	 * Create a request matcher for the given pattern.
+	 *
+	 * <p>
+	 * For example, you might do something like the following: <code>
+	 *     builder.matcher("/controller/**")
+	 * </code>
+	 * @param pattern the pattern to use, typically an Ant path
+	 * @return a {@link RequestMatcher} that matches on the given {@code pattern}
+	 */
+	RequestMatcher matcher(String pattern);
+
+	/**
+	 * Create a request matcher for the given pattern.
+	 *
+	 * <p>
+	 * For example, you might do something like the following: <code>
+	 *     builder.matcher(HttpMethod.GET, "/controller/**")
+	 * </code>
+	 * @param method the HTTP method to use
+	 * @param pattern the pattern to use, typically an Ant path
+	 * @return a {@link RequestMatcher} that matches on the given HTTP {@code method} and
+	 * {@code pattern}
+	 */
+	RequestMatcher matcher(HttpMethod method, String pattern);
+
+	/**
+	 * Create a request matcher that matches any request
+	 * @return a {@link RequestMatcher} that matches any request
+	 */
+	default RequestMatcher any() {
+		return AnyRequestMatcher.INSTANCE;
+	}
+
+	/**
+	 * Create an array request matchers, one for each of the given patterns.
+	 *
+	 * <p>
+	 * For example, you might do something like the following: <code>
+	 *     builder.matcher("/controller-one/**", "/controller-two/**")
+	 * </code>
+	 * @param patterns the patterns to use, typically Ant paths
+	 * @return a list of {@link RequestMatcher} that match on the given {@code pattern}
+	 */
+	default List<RequestMatcher> matchers(String... patterns) {
+		List<RequestMatcher> matchers = new ArrayList<>();
+		for (String pattern : patterns) {
+			matchers.add(matcher(pattern));
+		}
+		return matchers;
+	}
+
+	/**
+	 * Create an array request matchers, one for each of the given patterns.
+	 *
+	 * <p>
+	 * For example, you might do something like the following: <code>
+	 *     builder.matcher(HttpMethod.POST, "/controller-one/**", "/controller-two/**")
+	 * </code>
+	 * @param method the HTTP method to use
+	 * @param patterns the patterns to use, typically Ant paths
+	 * @return a list of {@link RequestMatcher} that match on the given HTTP
+	 * {@code method} and {@code pattern}
+	 */
+	default List<RequestMatcher> matchers(HttpMethod method, String... patterns) {
+		List<RequestMatcher> matchers = new ArrayList<>();
+		for (String pattern : patterns) {
+			matchers.add(matcher(method, pattern));
+		}
+		return matchers;
+	}
+
+}

+ 215 - 0
config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestMatcherBuilders.java

@@ -0,0 +1,215 @@
+/*
+ * Copyright 2002-2023 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.configurers;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
+import org.springframework.web.servlet.DispatcherServlet;
+
+/**
+ * A factory for constructing {@link RequestMatcherBuilder} instances
+ *
+ * @author Josh Cummings
+ * @since 6.2
+ */
+final class RequestMatcherBuilders {
+
+	private static final String HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME = "mvcHandlerMappingIntrospector";
+
+	private static final String HANDLER_MAPPING_INTROSPECTOR = "org.springframework.web.servlet.handler.HandlerMappingIntrospector";
+
+	private static final boolean mvcPresent;
+
+	static {
+		mvcPresent = ClassUtils.isPresent(HANDLER_MAPPING_INTROSPECTOR, RequestMatcherBuilders.class.getClassLoader());
+	}
+
+	private static final Log logger = LogFactory.getLog(RequestMatcherBuilders.class);
+
+	private RequestMatcherBuilders() {
+
+	}
+
+	/**
+	 * Create the default {@link RequestMatcherBuilder} for use by Spring Security DSLs.
+	 *
+	 * <p>
+	 * If Spring MVC is not present on the classpath or if there is no
+	 * {@link DispatcherServlet}, this method will return an Ant-based builder.
+	 *
+	 * <p>
+	 * If the servlet configuration has only {@link DispatcherServlet} with a single
+	 * mapping (for example `/` or `/path/*`), then this method will return an MVC-based
+	 * builder.
+	 *
+	 * <p>
+	 * If the servlet configuration maps {@link DispatcherServlet} to a path and also has
+	 * other servlets, this will throw an exception. In that case, an application should
+	 * instead use the {@link RequestMatcherBuilders#createForServletPattern} ideally with
+	 * the associated
+	 * {@link org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer}
+	 * to create builders by servlet path.
+	 *
+	 * <p>
+	 * Otherwise, (namely if {@link DispatcherServlet} is root), this method will return a
+	 * builder that delegates to an Ant or Mvc builder at runtime.
+	 * @param context the application context
+	 * @return the appropriate {@link RequestMatcherBuilder} based on application
+	 * configuration
+	 */
+	static RequestMatcherBuilder createDefault(ApplicationContext context) {
+		if (!mvcPresent) {
+			logger.trace("Defaulting to Ant matching since Spring MVC is not on the classpath");
+			return AntPathRequestMatcherBuilder.absolute();
+		}
+		if (!context.containsBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)) {
+			logger.trace("Defaulting to Ant matching since Spring MVC is not fully configured");
+			return AntPathRequestMatcherBuilder.absolute();
+		}
+		ServletRegistrationCollection registrations = ServletRegistrationCollection.registrations(context);
+		if (registrations.isEmpty()) {
+			logger.trace("Defaulting to MVC matching since Spring MVC is on the class path and no servlet "
+					+ "information is available");
+			return AntPathRequestMatcherBuilder.absolute();
+		}
+		ServletRegistrationCollection dispatcherServlets = registrations.dispatcherServlets();
+		if (dispatcherServlets.isEmpty()) {
+			logger.trace("Defaulting to Ant matching since there is no DispatcherServlet configured");
+			return AntPathRequestMatcherBuilder.absolute();
+		}
+		ServletRegistrationCollection.ServletPath servletPath = registrations.deduceOneServletPath();
+		if (servletPath != null) {
+			String message = "Defaulting to MVC matching since DispatcherServlet [%s] is the only servlet mapping";
+			logger.trace(String.format(message, servletPath.path()));
+			return MvcRequestMatcherBuilder.relativeTo(context, servletPath.path());
+		}
+		servletPath = dispatcherServlets.deduceOneServletPath();
+		if (servletPath == null) {
+			logger.trace("Did not choose a default since there is more than one DispatcherServlet mapping");
+			String message = String.format("""
+					This method cannot decide whether these patterns are Spring MVC patterns or not
+					since your servlet configuration has multiple Spring MVC servlet mappings.
+
+					For your reference, here is your servlet configuration: %s
+
+					To address this, you need to specify the servlet path for each endpoint.
+					You can use .forServletPattern in conjunction with requestMatchers do to this
+					like so:
+
+					@Bean
+					SecurityFilterChain appSecurity(HttpSecurity http) throws Exception {
+						http
+							.authorizeHttpRequests((authorize) -> authorize
+								.forServletPattern("/mvc-one/*", (one) -> one
+									.requestMatchers("/controller/**", "/endpoints/**"
+								)...
+								.forServletPattern("/mvc-two/*", (two) -> two
+									.requestMatchers("/other/**", "/controllers/**")...
+								)
+								.forServletPattern("/h2-console/*", (h2) -> h2
+									.requestMatchers("/**")...
+								)
+							)
+							// ...
+						return http.build();
+					}
+					""", registrations);
+			return new ErrorRequestMatcherBuilder(message);
+		}
+		if (servletPath.path() != null) {
+			logger.trace("Did not choose a default since there is a non-root DispatcherServlet mapping");
+			String message = String.format("""
+					This method cannot decide whether these patterns are Spring MVC patterns or not
+					since your Spring MVC mapping is mapped to a path and you have other servlet mappings.
+
+					For your reference, here is your servlet configuration: %s
+
+					To address this, you need to specify the servlet path for each endpoint.
+					You can use .forServletPattern in conjunction with requestMatchers do to this
+					like so:
+
+					@Bean
+					SecurityFilterChain appSecurity(HttpSecurity http) throws Exception {
+						http
+							.authorizeHttpRequests((authorize) -> authorize
+								.forServletPattern("/mvc/*", (mvc) -> mvc
+									.requestMatchers("/controller/**", "/endpoints/**")...
+								)
+								.forServletPattern("/h2-console/*", (h2) -> h2
+									.requestMatchers("/**")...
+								)
+							)
+							// ...
+						return http.build();
+					}
+					""", registrations);
+			return new ErrorRequestMatcherBuilder(message);
+		}
+		logger.trace("Defaulting to request-time checker since DispatcherServlet is mapped to root, but there are also "
+				+ "other servlet mappings");
+		return new DispatcherServletDelegatingRequestMatcherBuilder(MvcRequestMatcherBuilder.absolute(context),
+				AntPathRequestMatcherBuilder.absolute(), registrations);
+	}
+
+	static RequestMatcherBuilder createForServletPattern(ApplicationContext context, String pattern) {
+		Assert.notNull(pattern, "pattern cannot be null");
+		ServletRegistrationCollection registrations = ServletRegistrationCollection.registrations(context);
+		ServletRegistrationCollection.Registration registration = registrations.registrationByMapping(pattern);
+		Assert.notNull(registration, () -> String
+			.format("The given pattern %s doesn't seem to match any configured servlets: %s", pattern, registrations));
+		boolean isPathPattern = pattern.startsWith("/") && pattern.endsWith("/*");
+		if (isPathPattern) {
+			String path = pattern.substring(0, pattern.length() - 2);
+			return (registration.isDispatcherServlet()) ? MvcRequestMatcherBuilder.relativeTo(context, path)
+					: AntPathRequestMatcherBuilder.relativeTo(path);
+		}
+		return (registration.isDispatcherServlet()) ? MvcRequestMatcherBuilder.absolute(context)
+				: AntPathRequestMatcherBuilder.absolute();
+	}
+
+	private static class ErrorRequestMatcherBuilder implements RequestMatcherBuilder {
+
+		private final String errorMessage;
+
+		ErrorRequestMatcherBuilder(String errorMessage) {
+			this.errorMessage = errorMessage;
+		}
+
+		@Override
+		public RequestMatcher matcher(String pattern) {
+			throw new IllegalArgumentException(this.errorMessage);
+		}
+
+		@Override
+		public RequestMatcher matcher(HttpMethod method, String pattern) {
+			throw new IllegalArgumentException(this.errorMessage);
+		}
+
+		@Override
+		public RequestMatcher any() {
+			throw new IllegalArgumentException(this.errorMessage);
+		}
+
+	}
+
+}

+ 43 - 0
config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletPatternRequestMatcher.java

@@ -0,0 +1,43 @@
+/*
+ * Copyright 2002-2023 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.configurers;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.Assert;
+
+final class ServletPatternRequestMatcher implements RequestMatcher {
+
+	final String pattern;
+
+	ServletPatternRequestMatcher(String pattern) {
+		Assert.notNull(pattern, "pattern cannot be null");
+		this.pattern = pattern;
+	}
+
+	@Override
+	public boolean matches(HttpServletRequest request) {
+		return this.pattern.equals(request.getHttpServletMapping().getPattern());
+	}
+
+	@Override
+	public String toString() {
+		return String.format("ServletPattern [pattern='%s']", this.pattern);
+	}
+
+}

+ 152 - 0
config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletRegistrationCollection.java

@@ -0,0 +1,152 @@
+/*
+ * Copyright 2002-2023 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.configurers;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletRegistration;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.CollectionUtils;
+import org.springframework.web.context.WebApplicationContext;
+
+final class ServletRegistrationCollection {
+
+	private List<Registration> registrations;
+
+	private ServletRegistrationCollection() {
+		this.registrations = Collections.emptyList();
+	}
+
+	private ServletRegistrationCollection(List<Registration> registrations) {
+		this.registrations = registrations;
+	}
+
+	static ServletRegistrationCollection registrations(ApplicationContext context) {
+		if (!(context instanceof WebApplicationContext web)) {
+			return new ServletRegistrationCollection();
+		}
+		ServletContext servletContext = web.getServletContext();
+		if (servletContext == null) {
+			return new ServletRegistrationCollection();
+		}
+		Map<String, ? extends ServletRegistration> registrations = servletContext.getServletRegistrations();
+		if (registrations == null) {
+			return new ServletRegistrationCollection();
+		}
+		List<Registration> filtered = new ArrayList<>();
+		for (ServletRegistration registration : registrations.values()) {
+			Collection<String> mappings = registration.getMappings();
+			if (!CollectionUtils.isEmpty(mappings)) {
+				filtered.add(new Registration(registration));
+			}
+		}
+		return new ServletRegistrationCollection(filtered);
+	}
+
+	boolean isEmpty() {
+		return this.registrations.isEmpty();
+	}
+
+	Registration registrationByName(String name) {
+		for (Registration registration : this.registrations) {
+			if (registration.registration().getName().equals(name)) {
+				return registration;
+			}
+		}
+		return null;
+	}
+
+	Registration registrationByMapping(String target) {
+		for (Registration registration : this.registrations) {
+			for (String mapping : registration.registration().getMappings()) {
+				if (target.equals(mapping)) {
+					return registration;
+				}
+			}
+		}
+		return null;
+	}
+
+	ServletRegistrationCollection dispatcherServlets() {
+		List<Registration> dispatcherServlets = new ArrayList<>();
+		for (Registration registration : this.registrations) {
+			if (registration.isDispatcherServlet()) {
+				dispatcherServlets.add(registration);
+			}
+		}
+		return new ServletRegistrationCollection(dispatcherServlets);
+	}
+
+	ServletPath deduceOneServletPath() {
+		if (this.registrations.size() > 1) {
+			return null;
+		}
+		ServletRegistration registration = this.registrations.iterator().next().registration();
+		if (registration.getMappings().size() > 1) {
+			return null;
+		}
+		String mapping = registration.getMappings().iterator().next();
+		if ("/".equals(mapping)) {
+			return new ServletPath();
+		}
+		if (mapping.endsWith("/*")) {
+			return new ServletPath(mapping.substring(0, mapping.length() - 2));
+		}
+		return null;
+	}
+
+	@Override
+	public String toString() {
+		Map<String, Collection<String>> mappings = new LinkedHashMap<>();
+		for (Registration registration : this.registrations) {
+			mappings.put(registration.registration().getClassName(), registration.registration().getMappings());
+		}
+		return mappings.toString();
+	}
+
+	record Registration(ServletRegistration registration) {
+		boolean isDispatcherServlet() {
+			Class<?> dispatcherServlet = ClassUtils
+				.resolveClassName("org.springframework.web.servlet.DispatcherServlet", null);
+			try {
+				Class<?> clazz = Class.forName(this.registration.getClassName());
+				if (dispatcherServlet.isAssignableFrom(clazz)) {
+					return true;
+				}
+			}
+			catch (ClassNotFoundException ex) {
+				return false;
+			}
+			return false;
+		}
+	}
+
+	record ServletPath(String path) {
+		ServletPath() {
+			this(null);
+		}
+	}
+
+}

+ 7 - 0
config/src/test/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryNoMvcTests.java

@@ -25,8 +25,12 @@ import org.springframework.http.HttpMethod;
 import org.springframework.security.test.support.ClassPathExclusions;
 import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.web.context.WebApplicationContext;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
 
 /**
  * Tests for {@link AbstractRequestMatcherRegistry} with no Spring MVC in the classpath
@@ -41,6 +45,9 @@ public class AbstractRequestMatcherRegistryNoMvcTests {
 	@BeforeEach
 	public void setUp() {
 		this.matcherRegistry = new TestRequestMatcherRegistry();
+		WebApplicationContext context = mock(WebApplicationContext.class);
+		given(context.getBeanNamesForType((Class<?>) any())).willReturn(new String[0]);
+		this.matcherRegistry.setApplicationContext(context);
 	}
 
 	@Test

+ 0 - 6
config/src/test/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryTests.java

@@ -163,12 +163,6 @@ public class AbstractRequestMatcherRegistryTests {
 		assertThat(requestMatchers).isNotEmpty();
 		assertThat(requestMatchers).hasSize(1);
 		assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class);
-		servletContext.addServlet("servletOne", Servlet.class);
-		servletContext.addServlet("servletTwo", Servlet.class);
-		requestMatchers = this.matcherRegistry.requestMatchers("/**");
-		assertThat(requestMatchers).isNotEmpty();
-		assertThat(requestMatchers).hasSize(1);
-		assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class);
 	}
 
 	@Test

+ 349 - 0
config/src/test/java/org/springframework/security/config/annotation/web/configurers/AbstractRequestMatcherBuilderRegistryTests.java

@@ -0,0 +1,349 @@
+/*
+ * Copyright 2002-2023 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.configurers;
+
+import java.util.List;
+import java.util.function.Consumer;
+
+import jakarta.servlet.Servlet;
+import jakarta.servlet.ServletContext;
+import org.assertj.core.api.AbstractObjectAssert;
+import org.assertj.core.api.ObjectAssert;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.config.MockServletContext;
+import org.springframework.security.config.annotation.ObjectPostProcessor;
+import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
+import org.springframework.security.web.util.matcher.AndRequestMatcher;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.web.context.support.GenericWebApplicationContext;
+import org.springframework.web.servlet.DispatcherServlet;
+import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Tests for {@link AbstractRequestMatcherBuilderRegistry}
+ */
+class AbstractRequestMatcherBuilderRegistryTests {
+
+	@Test
+	void defaultServletMatchersWhenDefaultDispatcherServletThenMvc() {
+		MockServletContext servletContext = MockServletContext.mvc();
+		List<RequestMatcher> matchers = defaultServlet(servletContext).requestMatchers("/mvc").matchers;
+		assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class);
+		assertThatMvc(matchers).servletPath().isNull();
+		assertThatMvc(matchers).pattern().isEqualTo("/mvc");
+		assertThatMvc(matchers).method().isNull();
+	}
+
+	@Test
+	void defaultServletHttpMethodMatchersWhenDefaultDispatcherServletThenMvc() {
+		MockServletContext servletContext = MockServletContext.mvc();
+		List<RequestMatcher> matchers = defaultServlet(servletContext).requestMatchers(HttpMethod.GET, "/mvc").matchers;
+		assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class);
+		assertThatMvc(matchers).servletPath().isNull();
+		assertThatMvc(matchers).pattern().isEqualTo("/mvc");
+		assertThatMvc(matchers).method().isEqualTo(HttpMethod.GET);
+	}
+
+	@Test
+	void servletMatchersWhenPathDispatcherServletThenMvc() {
+		MockServletContext servletContext = new MockServletContext();
+		servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*");
+		List<RequestMatcher> matchers = servletPattern(servletContext, "/mvc/*")
+			.requestMatchers("/controller").matchers;
+		assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class);
+		assertThatMvc(matchers).servletPath().isEqualTo("/mvc");
+		assertThatMvc(matchers).pattern().isEqualTo("/controller");
+	}
+
+	@Test
+	void servletMatchersWhenAlsoExtraServletContainerMappingsThenMvc() {
+		MockServletContext servletContext = new MockServletContext();
+		servletContext.addServlet("default", Servlet.class);
+		servletContext.addServlet("jspServlet", Servlet.class).addMapping("*.jsp", "*.jspx");
+		servletContext.addServlet("facesServlet", Servlet.class).addMapping("/faces/", "*.jsf", "*.faces", "*.xhtml");
+		servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*");
+		List<RequestMatcher> matchers = servletPattern(servletContext, "/mvc/*")
+			.requestMatchers("/controller").matchers;
+		assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class);
+		assertThatMvc(matchers).servletPath().isEqualTo("/mvc");
+		assertThatMvc(matchers).pattern().isEqualTo("/controller");
+	}
+
+	@Test
+	void defaultServletMatchersWhenOnlyDefaultServletThenAnt() {
+		MockServletContext servletContext = new MockServletContext();
+		servletContext.addServlet("default", Servlet.class).addMapping("/");
+		List<RequestMatcher> matchers = defaultServlet(servletContext).requestMatchers("/controller").matchers;
+		assertThat(matchers).hasSize(1).hasOnlyElementsOfType(AntPathRequestMatcher.class);
+		assertThatAnt(matchers).pattern().isEqualTo("/controller");
+	}
+
+	@Test
+	void defaultDispatcherServletMatchersWhenNoHandlerMappingIntrospectorThenException() {
+		MockServletContext servletContext = MockServletContext.mvc();
+		assertThatExceptionOfType(NoSuchBeanDefinitionException.class)
+			.isThrownBy(() -> defaultServlet(servletContext, (context) -> {
+			}));
+	}
+
+	@Test
+	void dispatcherServletMatchersWhenNoHandlerMappingIntrospectorThenException() {
+		MockServletContext servletContext = new MockServletContext();
+		servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*");
+		assertThatExceptionOfType(NoSuchBeanDefinitionException.class)
+			.isThrownBy(() -> servletPattern(servletContext, (context) -> {
+			}, "/mvc/*"));
+	}
+
+	@Test
+	void matchersWhenNoDispatchServletThenAnt() {
+		MockServletContext servletContext = new MockServletContext();
+		servletContext.addServlet("default", Servlet.class).addMapping("/");
+		servletContext.addServlet("messageDispatcherServlet", Servlet.class).addMapping("/services/*");
+		List<RequestMatcher> matchers = defaultServlet(servletContext).requestMatchers("/services/endpoint").matchers;
+		assertThat(matchers).hasSize(1).hasOnlyElementsOfType(AntPathRequestMatcher.class);
+		assertThatAnt(matchers).pattern().isEqualTo("/services/endpoint");
+	}
+
+	@Test
+	void servletMatchersWhenMixedServletsThenDeterminesByServletPath() {
+		MockServletContext servletContext = MockServletContext.mvc();
+		servletContext.addServlet("messageDispatcherServlet", Servlet.class).addMapping("/services/*");
+		List<RequestMatcher> matchers = servletPattern(servletContext, "/services/*")
+			.requestMatchers("/endpoint").matchers;
+		assertThat(matchers).hasSize(1).hasOnlyElementsOfType(AntPathRequestMatcher.class);
+		assertThatAnt(matchers).pattern().isEqualTo("/services/endpoint");
+		matchers = defaultServlet(servletContext).requestMatchers("/controller").matchers;
+		assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class);
+		assertThatMvc(matchers).servletPath().isNull();
+		assertThatMvc(matchers).pattern().isEqualTo("/controller");
+	}
+
+	@Test
+	void servletMatchersWhenDispatcherServletNotDefaultThenDeterminesByServletPath() {
+		MockServletContext servletContext = new MockServletContext();
+		servletContext.addServlet("default", Servlet.class).addMapping("/");
+		servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*");
+		List<RequestMatcher> matchers = servletPattern(servletContext, "/mvc/*")
+			.requestMatchers("/controller").matchers;
+		assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class);
+		assertThatMvc(matchers).servletPath().isEqualTo("/mvc");
+		assertThatMvc(matchers).pattern().isEqualTo("/controller");
+		matchers = defaultServlet(servletContext).requestMatchers("/endpoint").matchers;
+		assertThat(matchers).hasSize(1).hasOnlyElementsOfType(AntPathRequestMatcher.class);
+		assertThatAnt(matchers).pattern().isEqualTo("/endpoint");
+	}
+
+	@Test
+	void servletHttpMatchersWhenDispatcherServletNotDefaultThenDeterminesByServletPath() {
+		MockServletContext servletContext = new MockServletContext();
+		servletContext.addServlet("default", Servlet.class).addMapping("/");
+		servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*");
+		List<RequestMatcher> matchers = servletPattern(servletContext, "/mvc/*").requestMatchers(HttpMethod.GET,
+				"/controller").matchers;
+		assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class);
+		assertThatMvc(matchers).method().isEqualTo(HttpMethod.GET);
+		assertThatMvc(matchers).servletPath().isEqualTo("/mvc");
+		assertThatMvc(matchers).pattern().isEqualTo("/controller");
+		matchers = defaultServlet(servletContext).requestMatchers(HttpMethod.GET, "/endpoint").matchers;
+		assertThat(matchers).hasSize(1).hasOnlyElementsOfType(AntPathRequestMatcher.class);
+		assertThatAnt(matchers).method().isEqualTo(HttpMethod.GET);
+		assertThatAnt(matchers).pattern().isEqualTo("/endpoint");
+	}
+
+	@Test
+	void servletMatchersWhenTwoDispatcherServletsThenDeterminesByServletPath() {
+		MockServletContext servletContext = MockServletContext.mvc();
+		servletContext.addServlet("two", DispatcherServlet.class).addMapping("/other/*");
+		List<RequestMatcher> matchers = defaultServlet(servletContext).requestMatchers("/controller").matchers;
+		assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class);
+		assertThatMvc(matchers).servletPath().isNull();
+		assertThatMvc(matchers).pattern().isEqualTo("/controller");
+		matchers = servletPattern(servletContext, "/other/*").requestMatchers("/endpoint").matchers;
+		assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class);
+		assertThatMvc(matchers).servletPath().isEqualTo("/other");
+		assertThatMvc(matchers).pattern().isEqualTo("/endpoint");
+	}
+
+	@Test
+	void servletMatchersWhenMoreThanOneMappingThenDeterminesByServletPath() {
+		MockServletContext servletContext = new MockServletContext();
+		servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/", "/two/*");
+		List<RequestMatcher> matchers = defaultServlet(servletContext).requestMatchers("/controller").matchers;
+		assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class);
+		assertThatMvc(matchers).servletPath().isNull();
+		assertThatMvc(matchers).pattern().isEqualTo("/controller");
+		matchers = servletPattern(servletContext, "/two/*").requestMatchers("/endpoint").matchers;
+		assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class);
+		assertThatMvc(matchers).servletPath().isEqualTo("/two");
+		assertThatMvc(matchers).pattern().isEqualTo("/endpoint");
+	}
+
+	@Test
+	void servletMatchersWhenMoreThanOneMappingAndDefaultServletsThenDeterminesByServletPath() {
+		MockServletContext servletContext = new MockServletContext();
+		servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/", "/two/*");
+		servletContext.addServlet("jspServlet", Servlet.class).addMapping("*.jsp", "*.jspx");
+		List<RequestMatcher> matchers = defaultServlet(servletContext).requestMatchers("/controller").matchers;
+		assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class);
+		assertThatMvc(matchers).servletPath().isNull();
+		assertThatMvc(matchers).pattern().isEqualTo("/controller");
+		matchers = servletPattern(servletContext, "/two/*").requestMatchers("/endpoint").matchers;
+		assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class);
+		assertThatMvc(matchers).servletPath().isEqualTo("/two");
+		assertThatMvc(matchers).pattern().isEqualTo("/endpoint");
+	}
+
+	@Test
+	void defaultServletWhenDispatcherServletThenMvc() {
+		MockServletContext servletContext = MockServletContext.mvc();
+		servletContext.addServlet("messageDispatcherServlet", Servlet.class).addMapping("/services/*");
+		List<RequestMatcher> matchers = defaultServlet(servletContext).requestMatchers("/controller").matchers;
+		assertThat(matchers).hasSize(1).hasOnlyElementsOfType(MvcRequestMatcher.class);
+		assertThatMvc(matchers).servletPath().isNull();
+		assertThatMvc(matchers).pattern().isEqualTo("/controller");
+		matchers = servletPattern(servletContext, "/services/*").requestMatchers("/endpoint").matchers;
+		assertThat(matchers).hasSize(1).hasOnlyElementsOfType(AntPathRequestMatcher.class);
+		assertThatAnt(matchers).pattern().isEqualTo("/services/endpoint");
+	}
+
+	@Test
+	void defaultServletWhenNoDefaultServletThenException() {
+		MockServletContext servletContext = new MockServletContext();
+		servletContext.addServlet("messageDispatcherServlet", Servlet.class).addMapping("/services/*");
+		assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> defaultServlet(servletContext));
+	}
+
+	@Test
+	void servletPathWhenNoMatchingServletThenException() {
+		MockServletContext servletContext = MockServletContext.mvc();
+		assertThatExceptionOfType(IllegalArgumentException.class)
+			.isThrownBy(() -> servletPattern(servletContext, "/wrong/*"));
+	}
+
+	TestServletRequestMatcherRegistry defaultServlet(ServletContext servletContext) {
+		return servletPattern(servletContext, "/");
+	}
+
+	TestServletRequestMatcherRegistry defaultServlet(ServletContext servletContext,
+			Consumer<GenericWebApplicationContext> consumer) {
+		return servletPattern(servletContext, consumer, "/");
+	}
+
+	TestServletRequestMatcherRegistry servletPattern(ServletContext servletContext, String pattern) {
+		return servletPattern(servletContext, (context) -> {
+			context.registerBean("mvcHandlerMappingIntrospector", HandlerMappingIntrospector.class);
+			context.registerBean(ObjectPostProcessor.class, () -> mock(ObjectPostProcessor.class));
+		}, pattern);
+	}
+
+	TestServletRequestMatcherRegistry servletPattern(ServletContext servletContext,
+			Consumer<GenericWebApplicationContext> consumer, String pattern) {
+		GenericWebApplicationContext context = new GenericWebApplicationContext(servletContext);
+		consumer.accept(context);
+		context.refresh();
+		return new TestServletRequestMatcherRegistry(context, pattern);
+	}
+
+	static MvcRequestMatcherAssert assertThatMvc(List<RequestMatcher> matchers) {
+		RequestMatcher matcher = matchers.get(0);
+		if (matcher instanceof AndRequestMatcher matching) {
+			List<RequestMatcher> and = (List<RequestMatcher>) ReflectionTestUtils.getField(matching, "requestMatchers");
+			assertThat(and).hasSize(2);
+			assertThat(and.get(1)).isInstanceOf(MvcRequestMatcher.class);
+			return new MvcRequestMatcherAssert((MvcRequestMatcher) and.get(1));
+		}
+		assertThat(matcher).isInstanceOf(MvcRequestMatcher.class);
+		return new MvcRequestMatcherAssert((MvcRequestMatcher) matcher);
+	}
+
+	static AntPathRequestMatcherAssert assertThatAnt(List<RequestMatcher> matchers) {
+		RequestMatcher matcher = matchers.get(0);
+		if (matcher instanceof AndRequestMatcher matching) {
+			List<RequestMatcher> and = (List<RequestMatcher>) ReflectionTestUtils.getField(matching, "requestMatchers");
+			assertThat(and).hasSize(2);
+			assertThat(and.get(1)).isInstanceOf(AntPathRequestMatcher.class);
+			return new AntPathRequestMatcherAssert((AntPathRequestMatcher) and.get(1));
+		}
+		assertThat(matcher).isInstanceOf(AntPathRequestMatcher.class);
+		return new AntPathRequestMatcherAssert((AntPathRequestMatcher) matcher);
+	}
+
+	static final class TestServletRequestMatcherRegistry
+			extends AbstractRequestMatcherBuilderRegistry<TestServletRequestMatcherRegistry> {
+
+		List<RequestMatcher> matchers;
+
+		TestServletRequestMatcherRegistry(ApplicationContext context, String pattern) {
+			super(context, RequestMatcherBuilders.createForServletPattern(context, pattern));
+		}
+
+		@Override
+		protected TestServletRequestMatcherRegistry chainRequestMatchers(List<RequestMatcher> requestMatchers) {
+			this.matchers = requestMatchers;
+			return this;
+		}
+
+	}
+
+	static final class MvcRequestMatcherAssert extends ObjectAssert<MvcRequestMatcher> {
+
+		private MvcRequestMatcherAssert(MvcRequestMatcher matcher) {
+			super(matcher);
+		}
+
+		AbstractObjectAssert<?, ?> servletPath() {
+			return extracting("servletPath");
+		}
+
+		AbstractObjectAssert<?, ?> pattern() {
+			return extracting("pattern");
+		}
+
+		AbstractObjectAssert<?, ?> method() {
+			return extracting("method");
+		}
+
+	}
+
+	static final class AntPathRequestMatcherAssert extends ObjectAssert<AntPathRequestMatcher> {
+
+		private AntPathRequestMatcherAssert(AntPathRequestMatcher matcher) {
+			super(matcher);
+		}
+
+		AbstractObjectAssert<?, ?> pattern() {
+			return extracting("pattern");
+		}
+
+		AbstractObjectAssert<?, ?> method() {
+			return extracting("httpMethod");
+		}
+
+	}
+
+}

+ 362 - 5
config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java

@@ -18,6 +18,7 @@ package org.springframework.security.config.annotation.web.configurers;
 
 import java.util.function.Supplier;
 
+import jakarta.servlet.Servlet;
 import jakarta.servlet.http.HttpServletRequest;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -26,6 +27,7 @@ import org.springframework.beans.factory.BeanCreationException;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpMethod;
 import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
 import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
 import org.springframework.security.authentication.RememberMeAuthenticationToken;
@@ -33,6 +35,7 @@ import org.springframework.security.authentication.TestAuthentication;
 import org.springframework.security.authorization.AuthorizationDecision;
 import org.springframework.security.authorization.AuthorizationEventPublisher;
 import org.springframework.security.authorization.AuthorizationManager;
+import org.springframework.security.config.MockServletContext;
 import org.springframework.security.config.annotation.ObjectPostProcessor;
 import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@@ -58,6 +61,7 @@ import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.servlet.DispatcherServlet;
 import org.springframework.web.servlet.config.annotation.EnableWebMvc;
 import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
 
@@ -71,6 +75,7 @@ import static org.springframework.security.test.web.servlet.request.SecurityMock
 import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
 import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.head;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 
@@ -121,7 +126,7 @@ public class AuthorizeHttpRequestsConfigurerTests {
 	public void configureWhenMvcMatcherAfterAnyRequestThenException() {
 		assertThatExceptionOfType(BeanCreationException.class)
 			.isThrownBy(() -> this.spring.register(AfterAnyRequestConfig.class).autowire())
-			.withMessageContaining("Can't configure mvcMatchers after anyRequest");
+			.withMessageContaining("Can't configure requestMatchers after anyRequest");
 	}
 
 	@Test
@@ -362,7 +367,7 @@ public class AuthorizeHttpRequestsConfigurerTests {
 
 	@Test
 	public void getWhenServletPathRoleAdminConfiguredAndRoleIsUserThenRespondsWithForbidden() throws Exception {
-		this.spring.register(ServletPathConfig.class, BasicController.class).autowire();
+		this.spring.register(MvcServletPathConfig.class, BasicController.class).autowire();
 		// @formatter:off
 		MockHttpServletRequestBuilder requestWithUser = get("/spring/")
 				.servletPath("/spring")
@@ -375,7 +380,7 @@ public class AuthorizeHttpRequestsConfigurerTests {
 	@Test
 	public void getWhenServletPathRoleAdminConfiguredAndRoleIsUserAndWithoutServletPathThenRespondsWithForbidden()
 			throws Exception {
-		this.spring.register(ServletPathConfig.class, BasicController.class).autowire();
+		this.spring.register(MvcServletPathConfig.class, BasicController.class).autowire();
 		// @formatter:off
 		MockHttpServletRequestBuilder requestWithUser = get("/")
 				.with(user("user")
@@ -386,7 +391,7 @@ public class AuthorizeHttpRequestsConfigurerTests {
 
 	@Test
 	public void getWhenServletPathRoleAdminConfiguredAndRoleIsAdminThenRespondsWithOk() throws Exception {
-		this.spring.register(ServletPathConfig.class, BasicController.class).autowire();
+		this.spring.register(MvcServletPathConfig.class, BasicController.class).autowire();
 		// @formatter:off
 		MockHttpServletRequestBuilder requestWithAdmin = get("/spring/")
 				.servletPath("/spring")
@@ -596,6 +601,200 @@ public class AuthorizeHttpRequestsConfigurerTests {
 		this.mvc.perform(requestWithUser).andExpect(status().isForbidden());
 	}
 
+	@Test
+	public void configureWhenNoDispatcherServletThenSucceeds() throws Exception {
+		MockServletContext servletContext = new MockServletContext();
+		servletContext.addServlet("default", Servlet.class).addMapping("/");
+		this.spring.register(AuthorizeHttpRequestsConfig.class)
+			.postProcessor((context) -> context.setServletContext(servletContext))
+			.autowire();
+		this.mvc.perform(get("/path")).andExpect(status().isNotFound());
+	}
+
+	@Test
+	public void configureWhenOnlyDispatcherServletThenSucceeds() throws Exception {
+		MockServletContext servletContext = new MockServletContext();
+		servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*");
+		this.spring.register(AuthorizeHttpRequestsConfig.class)
+			.postProcessor((context) -> context.setServletContext(servletContext))
+			.autowire();
+		this.mvc.perform(get("/mvc/path").servletPath("/mvc")).andExpect(status().isNotFound());
+		this.mvc.perform(get("/mvc")).andExpect(status().isUnauthorized());
+	}
+
+	@Test
+	public void configureWhenMultipleServletsThenSucceeds() throws Exception {
+		MockServletContext servletContext = MockServletContext.mvc();
+		servletContext.addServlet("path", Servlet.class).addMapping("/path/*");
+		this.spring.register(AuthorizeHttpRequestsConfig.class)
+			.postProcessor((context) -> context.setServletContext(servletContext))
+			.autowire();
+		this.mvc.perform(get("/path").with(servletPath("/path"))).andExpect(status().isNotFound());
+	}
+
+	@Test
+	public void configureWhenAmbiguousServletsThenWiringException() {
+		MockServletContext servletContext = new MockServletContext();
+		servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*");
+		servletContext.addServlet("path", Servlet.class).addMapping("/path/*");
+		assertThatExceptionOfType(BeanCreationException.class)
+			.isThrownBy(() -> this.spring.register(AuthorizeHttpRequestsConfig.class)
+				.postProcessor((context) -> context.setServletContext(servletContext))
+				.autowire());
+	}
+
+	@Test
+	void defaultServletMatchersWhenDefaultServletThenPermits() throws Exception {
+		this.spring.register(DefaultServletConfig.class)
+			.postProcessor((context) -> context.setServletContext(MockServletContext.mvc()))
+			.autowire();
+		this.mvc.perform(get("/path/path").with(defaultServlet())).andExpect(status().isNotFound());
+		this.mvc.perform(get("/path/path").with(servletPath("/path"))).andExpect(status().isUnauthorized());
+	}
+
+	@Test
+	void defaultServletHttpMethodMatchersWhenDefaultServletThenPermits() throws Exception {
+		this.spring.register(DefaultServletConfig.class)
+			.postProcessor((context) -> context.setServletContext(MockServletContext.mvc()))
+			.autowire();
+		this.mvc.perform(get("/path/method").with(defaultServlet())).andExpect(status().isNotFound());
+		this.mvc.perform(head("/path/method").with(defaultServlet())).andExpect(status().isUnauthorized());
+		this.mvc.perform(get("/path/method").with(servletPath("/path"))).andExpect(status().isUnauthorized());
+	}
+
+	@Test
+	void defaultServletWhenNoDefaultServletThenWiringException() {
+		assertThatExceptionOfType(BeanCreationException.class)
+			.isThrownBy(() -> this.spring.register(DefaultServletConfig.class)
+				.postProcessor((context) -> context.setServletContext(new MockServletContext()))
+				.autowire());
+	}
+
+	@Test
+	void servletPathMatchersWhenMatchingServletThenPermits() throws Exception {
+		MockServletContext servletContext = MockServletContext.mvc();
+		servletContext.addServlet("path", Servlet.class).addMapping("/path/*");
+		this.spring.register(ServletPathConfig.class)
+			.postProcessor((context) -> context.setServletContext(servletContext))
+			.autowire();
+		this.mvc.perform(get("/path/path").with(servletPath("/path"))).andExpect(status().isNotFound());
+		this.mvc.perform(get("/path/path").with(defaultServlet())).andExpect(status().isUnauthorized());
+	}
+
+	@Test
+	void servletPathHttpMethodMatchersWhenMatchingServletThenPermits() throws Exception {
+		MockServletContext servletContext = MockServletContext.mvc();
+		servletContext.addServlet("path", Servlet.class).addMapping("/path/*");
+		this.spring.register(ServletPathConfig.class)
+			.postProcessor((context) -> context.setServletContext(servletContext))
+			.autowire();
+		this.mvc.perform(get("/path/method").with(servletPath("/path"))).andExpect(status().isNotFound());
+		this.mvc.perform(head("/path/method").with(servletPath("/path"))).andExpect(status().isUnauthorized());
+		this.mvc.perform(get("/path/method").with(defaultServlet())).andExpect(status().isUnauthorized());
+	}
+
+	@Test
+	void servletPathWhenNoMatchingPathThenWiringException() {
+		MockServletContext servletContext = MockServletContext.mvc();
+		assertThatExceptionOfType(BeanCreationException.class)
+			.isThrownBy(() -> this.spring.register(ServletPathConfig.class)
+				.postProcessor((context) -> context.setServletContext(servletContext))
+				.autowire());
+	}
+
+	@Test
+	void servletMappingMatchersWhenMatchingServletThenPermits() throws Exception {
+		MockServletContext servletContext = MockServletContext.mvc();
+		servletContext.addServlet("jsp", Servlet.class).addMapping("*.jsp");
+		this.spring.register(ServletMappingConfig.class)
+			.postProcessor((context) -> context.setServletContext(servletContext))
+			.autowire();
+		this.mvc.perform(get("/path/file.jsp").with(servletExtension(".jsp"))).andExpect(status().isNotFound());
+		this.mvc.perform(get("/path/file.jsp").with(defaultServlet())).andExpect(status().isUnauthorized());
+	}
+
+	@Test
+	void servletMappingHttpMethodMatchersWhenMatchingServletThenPermits() throws Exception {
+		MockServletContext servletContext = MockServletContext.mvc();
+		servletContext.addServlet("jsp", Servlet.class).addMapping("*.jsp");
+		this.spring.register(ServletMappingConfig.class)
+			.postProcessor((context) -> context.setServletContext(servletContext))
+			.autowire();
+		this.mvc.perform(get("/method/file.jsp").with(servletExtension(".jsp"))).andExpect(status().isNotFound());
+		this.mvc.perform(head("/method/file.jsp").with(servletExtension(".jsp"))).andExpect(status().isUnauthorized());
+		this.mvc.perform(get("/method/file.jsp").with(defaultServlet())).andExpect(status().isUnauthorized());
+	}
+
+	@Test
+	void servletMappingWhenNoMatchingExtensionThenWiringException() {
+		MockServletContext servletContext = MockServletContext.mvc();
+		assertThatExceptionOfType(BeanCreationException.class)
+			.isThrownBy(() -> this.spring.register(ServletMappingConfig.class)
+				.postProcessor((context) -> context.setServletContext(servletContext))
+				.autowire());
+	}
+
+	@Test
+	void anyRequestWhenUsedWithDefaultServletThenDoesNotWire() {
+		assertThatExceptionOfType(BeanCreationException.class)
+			.isThrownBy(() -> this.spring.register(MixedServletEndpointConfig.class).autowire())
+			.withMessageContaining("forServletPattern");
+	}
+
+	@Test
+	void servletWhenNoMatchingPathThenDenies() throws Exception {
+		MockServletContext servletContext = new MockServletContext();
+		servletContext.addServlet("default", Servlet.class).addMapping("/");
+		servletContext.addServlet("jspServlet", Servlet.class).addMapping("*.jsp");
+		servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*");
+		this.spring.register(DefaultServletAndServletPathConfig.class)
+			.postProcessor((context) -> context.setServletContext(servletContext))
+			.autowire();
+		this.mvc.perform(get("/js/color.js").with(servletPath("/js"))).andExpect(status().isUnauthorized());
+		this.mvc.perform(get("/mvc/controller").with(defaultServlet())).andExpect(status().isUnauthorized());
+		this.mvc.perform(get("/js/color.js").with(defaultServlet())).andExpect(status().isNotFound());
+		this.mvc.perform(get("/mvc/controller").with(servletPath("/mvc"))).andExpect(status().isUnauthorized());
+		this.mvc.perform(get("/mvc/controller").with(user("user")).with(servletPath("/mvc")))
+			.andExpect(status().isNotFound());
+	}
+
+	@Test
+	void permitAllWhenDefaultServletThenDoesNotWire() {
+		assertThatExceptionOfType(BeanCreationException.class)
+			.isThrownBy(() -> this.spring.register(MixedServletPermitAllConfig.class).autowire())
+			.withMessageContaining("forServletPattern");
+	}
+
+	static RequestPostProcessor defaultServlet() {
+		return (request) -> {
+			String uri = request.getRequestURI();
+			request.setHttpServletMapping(TestMockHttpServletMappings.defaultMapping());
+			request.setServletPath(uri);
+			request.setPathInfo("");
+			return request;
+		};
+	}
+
+	static RequestPostProcessor servletPath(String path) {
+		return (request) -> {
+			String uri = request.getRequestURI();
+			request.setHttpServletMapping(TestMockHttpServletMappings.path(request, path));
+			request.setServletPath(path);
+			request.setPathInfo(uri.substring(path.length()));
+			return request;
+		};
+	}
+
+	static RequestPostProcessor servletExtension(String extension) {
+		return (request) -> {
+			String uri = request.getRequestURI();
+			request.setHttpServletMapping(TestMockHttpServletMappings.extension(request, extension));
+			request.setServletPath(uri);
+			request.setPathInfo("");
+			return request;
+		};
+	}
+
 	@Configuration
 	@EnableWebSecurity
 	static class GrantedAuthorityDefaultHasRoleConfig {
@@ -693,6 +892,7 @@ public class AuthorizeHttpRequestsConfigurerTests {
 
 	@Configuration
 	@EnableWebSecurity
+	@EnableWebMvc
 	static class AfterAnyRequestConfig {
 
 		@Bean
@@ -954,7 +1154,7 @@ public class AuthorizeHttpRequestsConfigurerTests {
 	@Configuration
 	@EnableWebMvc
 	@EnableWebSecurity
-	static class ServletPathConfig {
+	static class MvcServletPathConfig {
 
 		@Bean
 		SecurityFilterChain filterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception {
@@ -1136,6 +1336,163 @@ public class AuthorizeHttpRequestsConfigurerTests {
 
 	}
 
+	@Configuration
+	@EnableWebSecurity
+	@EnableWebMvc
+	static class AuthorizeHttpRequestsConfig {
+
+		@Bean
+		SecurityFilterChain chain(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.httpBasic(withDefaults())
+				.authorizeHttpRequests((requests) -> requests
+					.requestMatchers("/path/**").permitAll()
+					.anyRequest().authenticated()
+				);
+			// @formatter:on
+			return http.build();
+		}
+
+	}
+
+	@Configuration
+	@EnableWebSecurity
+	@EnableWebMvc
+	static class DefaultServletConfig {
+
+		@Bean
+		SecurityFilterChain chain(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.httpBasic(withDefaults())
+				.authorizeHttpRequests((requests) -> requests
+					.forServletPattern("/", (root) -> root
+						.requestMatchers(HttpMethod.GET, "/path/method/**").permitAll()
+						.requestMatchers("/path/path/**").permitAll()
+						.anyRequest().authenticated()
+					)
+				);
+			// @formatter:on
+			return http.build();
+		}
+
+	}
+
+	@Configuration
+	@EnableWebSecurity
+	@EnableWebMvc
+	static class ServletPathConfig {
+
+		@Bean
+		SecurityFilterChain chain(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.httpBasic(withDefaults())
+				.authorizeHttpRequests((requests) -> requests
+					.forServletPattern("/path/*", (root) -> root
+						.requestMatchers(HttpMethod.GET, "/method/**").permitAll()
+						.requestMatchers("/path/**").permitAll()
+						.anyRequest().authenticated()
+					)
+				);
+			// @formatter:on
+			return http.build();
+		}
+
+	}
+
+	@Configuration
+	@EnableWebSecurity
+	@EnableWebMvc
+	static class ServletMappingConfig {
+
+		@Bean
+		SecurityFilterChain chain(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.httpBasic(withDefaults())
+				.authorizeHttpRequests((requests) -> requests
+					.forServletPattern("*.jsp", (jsp) -> jsp
+						.requestMatchers(HttpMethod.GET, "/method/**").permitAll()
+						.requestMatchers("/path/**").permitAll()
+						.anyRequest().authenticated()
+					)
+				);
+			// @formatter:on
+			return http.build();
+		}
+
+	}
+
+	@Configuration
+	@EnableWebSecurity
+	@EnableWebMvc
+	static class MixedServletEndpointConfig {
+
+		@Bean
+		SecurityFilterChain chain(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.httpBasic(withDefaults())
+				.authorizeHttpRequests((requests) -> requests
+					.forServletPattern("/", (root) -> root.anyRequest().permitAll())
+					.anyRequest().authenticated()
+				);
+			// @formatter:on
+			return http.build();
+		}
+
+	}
+
+	@Configuration
+	@EnableWebSecurity
+	@EnableWebMvc
+	static class MixedServletPermitAllConfig {
+
+		@Bean
+		SecurityFilterChain chain(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.formLogin((form) -> form.loginPage("/page").permitAll())
+				.authorizeHttpRequests((requests) -> requests
+					.forServletPattern("/", (root) -> root
+						.anyRequest().authenticated()
+					)
+				);
+			// @formatter:on
+			return http.build();
+		}
+
+	}
+
+	@Configuration
+	@EnableWebSecurity
+	@EnableWebMvc
+	static class DefaultServletAndServletPathConfig {
+
+		@Bean
+		SecurityFilterChain chain(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.httpBasic(withDefaults())
+				.authorizeHttpRequests((requests) -> requests
+					.forServletPattern("/", (root) -> root
+						.requestMatchers("/js/**", "/css/**").permitAll()
+					)
+					.forServletPattern("/mvc/*", (mvc) -> mvc
+						.requestMatchers("/controller/**").authenticated()
+					)
+					.forServletPattern("*.jsp", (jsp) -> jsp
+						.anyRequest().authenticated()
+					)
+				);
+			// @formatter:on
+			return http.build();
+		}
+
+	}
+
 	@Configuration
 	static class AuthorizationEventPublisherConfig {
 

+ 198 - 0
config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestMatcherBuildersTests.java

@@ -0,0 +1,198 @@
+/*
+ * Copyright 2002-2023 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.configurers;
+
+import java.util.List;
+import java.util.function.Consumer;
+
+import jakarta.servlet.Servlet;
+import jakarta.servlet.ServletContext;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.http.HttpMethod;
+import org.springframework.security.config.MockServletContext;
+import org.springframework.security.config.annotation.ObjectPostProcessor;
+import org.springframework.security.config.annotation.web.configurers.DispatcherServletDelegatingRequestMatcherBuilder.DispatcherServletDelegatingRequestMatcher;
+import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.web.context.support.GenericWebApplicationContext;
+import org.springframework.web.servlet.DispatcherServlet;
+import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.mockito.Mockito.mock;
+
+public class RequestMatcherBuildersTests {
+
+	@Test
+	void matchersWhenDefaultDispatcherServletThenMvc() {
+		MockServletContext servletContext = MockServletContext.mvc();
+		RequestMatcherBuilder builder = requestMatchersBuilder(servletContext);
+		List<RequestMatcher> matchers = builder.matchers("/mvc");
+		assertThat(matchers.get(0)).isInstanceOf(MvcRequestMatcher.class);
+		MvcRequestMatcher matcher = (MvcRequestMatcher) matchers.get(0);
+		assertThat(ReflectionTestUtils.getField(matcher, "servletPath")).isNull();
+		assertThat(ReflectionTestUtils.getField(matcher, "pattern")).isEqualTo("/mvc");
+	}
+
+	@Test
+	void httpMethodMatchersWhenDefaultDispatcherServletThenMvc() {
+		MockServletContext servletContext = MockServletContext.mvc();
+		RequestMatcherBuilder builder = requestMatchersBuilder(servletContext);
+		List<RequestMatcher> matchers = builder.matchers(HttpMethod.GET, "/mvc");
+		assertThat(matchers.get(0)).isInstanceOf(MvcRequestMatcher.class);
+		MvcRequestMatcher matcher = (MvcRequestMatcher) matchers.get(0);
+		assertThat(ReflectionTestUtils.getField(matcher, "servletPath")).isNull();
+		assertThat(ReflectionTestUtils.getField(matcher, "pattern")).isEqualTo("/mvc");
+		assertThat(ReflectionTestUtils.getField(matcher, "method")).isEqualTo(HttpMethod.GET);
+	}
+
+	@Test
+	void matchersWhenPathDispatcherServletThenMvc() {
+		MockServletContext servletContext = new MockServletContext();
+		servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*");
+		RequestMatcherBuilder builder = requestMatchersBuilder(servletContext);
+		List<RequestMatcher> matchers = builder.matchers("/controller");
+		assertThat(matchers.get(0)).isInstanceOf(MvcRequestMatcher.class);
+		MvcRequestMatcher matcher = (MvcRequestMatcher) matchers.get(0);
+		assertThat(ReflectionTestUtils.getField(matcher, "servletPath")).isEqualTo("/mvc");
+		assertThat(ReflectionTestUtils.getField(matcher, "pattern")).isEqualTo("/controller");
+	}
+
+	@Test
+	void matchersWhenAlsoExtraServletContainerMappingsThenRequiresServletPath() {
+		MockServletContext servletContext = new MockServletContext();
+		servletContext.addServlet("default", Servlet.class).addMapping("/");
+		servletContext.addServlet("jspServlet", Servlet.class).addMapping("*.jsp", "*.jspx");
+		servletContext.addServlet("facesServlet", Servlet.class).addMapping("/faces/", "*.jsf", "*.faces", "*.xhtml");
+		servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*");
+		assertThatExceptionOfType(IllegalArgumentException.class)
+			.isThrownBy(() -> requestMatchersBuilder(servletContext).matcher("/path"))
+			.withMessageContaining(".forServletPattern");
+	}
+
+	@Test
+	void matchersWhenOnlyDefaultServletThenAnt() {
+		MockServletContext servletContext = new MockServletContext();
+		servletContext.addServlet("default", Servlet.class).addMapping("/");
+		RequestMatcherBuilder builder = requestMatchersBuilder(servletContext);
+		List<RequestMatcher> matchers = builder.matchers("/controller");
+		assertThat(matchers.get(0)).isInstanceOf(AntPathRequestMatcher.class);
+		AntPathRequestMatcher matcher = (AntPathRequestMatcher) matchers.get(0);
+		assertThat(ReflectionTestUtils.getField(matcher, "pattern")).isEqualTo("/controller");
+	}
+
+	@Test
+	void matchersWhenNoHandlerMappingIntrospectorThenAnt() {
+		MockServletContext servletContext = MockServletContext.mvc();
+		RequestMatcherBuilder builder = requestMatchersBuilder(servletContext, (context) -> {
+		});
+		List<RequestMatcher> matchers = builder.matchers("/controller");
+		assertThat(matchers.get(0)).isInstanceOf(AntPathRequestMatcher.class);
+		AntPathRequestMatcher matcher = (AntPathRequestMatcher) matchers.get(0);
+		assertThat(ReflectionTestUtils.getField(matcher, "pattern")).isEqualTo("/controller");
+	}
+
+	@Test
+	void matchersWhenNoDispatchServletThenAnt() {
+		MockServletContext servletContext = new MockServletContext();
+		servletContext.addServlet("default", Servlet.class).addMapping("/");
+		servletContext.addServlet("messageDispatcherServlet", Servlet.class).addMapping("/services/*");
+		RequestMatcherBuilder builder = requestMatchersBuilder(servletContext);
+		List<RequestMatcher> matchers = builder.matchers("/services/endpoint");
+		assertThat(matchers.get(0)).isInstanceOf(AntPathRequestMatcher.class);
+		AntPathRequestMatcher matcher = (AntPathRequestMatcher) matchers.get(0);
+		assertThat(ReflectionTestUtils.getField(matcher, "pattern")).isEqualTo("/services/endpoint");
+	}
+
+	@Test
+	void matchersWhenMixedServletsThenServletPathDelegating() {
+		MockServletContext servletContext = MockServletContext.mvc();
+		servletContext.addServlet("messageDispatcherServlet", Servlet.class).addMapping("/services/*");
+		RequestMatcherBuilder builder = requestMatchersBuilder(servletContext);
+		assertThat(builder.matchers("/services/endpoint").get(0))
+			.isInstanceOf(DispatcherServletDelegatingRequestMatcher.class);
+	}
+
+	@Test
+	void matchersWhenDispatcherServletNotDefaultAndOtherServletsThenRequiresServletPath() {
+		MockServletContext servletContext = new MockServletContext();
+		servletContext.addServlet("default", Servlet.class).addMapping("/");
+		servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*");
+		assertThatExceptionOfType(IllegalArgumentException.class)
+			.isThrownBy(() -> requestMatchersBuilder(servletContext).matcher("/path/**"))
+			.withMessageContaining(".forServletPattern");
+	}
+
+	@Test
+	void httpMatchersWhenDispatcherServletNotDefaultAndOtherServletsThenRequiresServletPath() {
+		MockServletContext servletContext = new MockServletContext();
+		servletContext.addServlet("default", Servlet.class).addMapping("/");
+		servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/mvc/*");
+		assertThatExceptionOfType(IllegalArgumentException.class)
+			.isThrownBy(() -> requestMatchersBuilder(servletContext).matcher("/pattern"))
+			.withMessageContaining(".forServletPattern");
+	}
+
+	@Test
+	void matchersWhenTwoDispatcherServletsThenException() {
+		MockServletContext servletContext = MockServletContext.mvc();
+		servletContext.addServlet("two", DispatcherServlet.class).addMapping("/other/*");
+		assertThatExceptionOfType(IllegalArgumentException.class)
+			.isThrownBy(() -> requestMatchersBuilder(servletContext).matcher("/path/**"))
+			.withMessageContaining(".forServletPattern");
+	}
+
+	@Test
+	void matchersWhenMoreThanOneMappingThenException() {
+		MockServletContext servletContext = new MockServletContext();
+		servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/", "/two/*");
+		assertThatExceptionOfType(IllegalArgumentException.class)
+			.isThrownBy(() -> requestMatchersBuilder(servletContext).matcher("/path/**"))
+			.withMessageContaining(".forServletPattern");
+	}
+
+	@Test
+	void matchersWhenMoreThanOneMappingAndDefaultServletsThenRequiresServletPath() {
+		MockServletContext servletContext = new MockServletContext();
+		servletContext.addServlet("dispatcherServlet", DispatcherServlet.class).addMapping("/", "/two/*");
+		servletContext.addServlet("jspServlet", Servlet.class).addMapping("*.jsp", "*.jspx");
+		assertThatExceptionOfType(IllegalArgumentException.class)
+			.isThrownBy(() -> requestMatchersBuilder(servletContext).matcher("/path/**"))
+			.withMessageContaining(".forServletPattern");
+	}
+
+	RequestMatcherBuilder requestMatchersBuilder(ServletContext servletContext) {
+		return requestMatchersBuilder(servletContext, (context) -> {
+			context.registerBean("mvcHandlerMappingIntrospector", HandlerMappingIntrospector.class,
+					() -> mock(HandlerMappingIntrospector.class));
+			context.registerBean(ObjectPostProcessor.class, () -> mock(ObjectPostProcessor.class));
+		});
+	}
+
+	RequestMatcherBuilder requestMatchersBuilder(ServletContext servletContext,
+			Consumer<GenericWebApplicationContext> consumer) {
+		GenericWebApplicationContext context = new GenericWebApplicationContext(servletContext);
+		consumer.accept(context);
+		context.refresh();
+		return RequestMatcherBuilders.createDefault(context);
+	}
+
+}

+ 63 - 0
config/src/test/java/org/springframework/security/config/annotation/web/configurers/ServletPatternRequestMatcherTests.java

@@ -0,0 +1,63 @@
+/*
+ * Copyright 2002-2023 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.configurers;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.mock.web.MockHttpServletRequest;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link ServletPatternRequestMatcher}
+ */
+class ServletPatternRequestMatcherTests {
+
+	ServletPatternRequestMatcher matcher = new ServletPatternRequestMatcher("*.jsp");
+
+	@Test
+	void matchesWhenDefaultServletThenTrue() {
+		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/a/uri.jsp");
+		request.setHttpServletMapping(TestMockHttpServletMappings.extension(request, ".jsp"));
+		assertThat(this.matcher.matches(request)).isTrue();
+	}
+
+	@Test
+	void matchesWhenNotDefaultServletThenFalse() {
+		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/a/uri.jsp");
+		request.setHttpServletMapping(TestMockHttpServletMappings.path(request, "/a"));
+		request.setServletPath("/a/uri.jsp");
+		assertThat(this.matcher.matches(request)).isFalse();
+	}
+
+	@Test
+	void matcherWhenDefaultServletThenTrue() {
+		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/a/uri.jsp");
+		request.setHttpServletMapping(TestMockHttpServletMappings.extension(request, ".jsp"));
+		request.setServletPath("/a/uri.jsp");
+		assertThat(this.matcher.matcher(request).isMatch()).isTrue();
+	}
+
+	@Test
+	void matcherWhenNotDefaultServletThenFalse() {
+		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/a/uri.jsp");
+		request.setHttpServletMapping(TestMockHttpServletMappings.path(request, "/a"));
+		request.setServletPath("/a/uri.jsp");
+		assertThat(this.matcher.matcher(request).isMatch()).isFalse();
+	}
+
+}

+ 46 - 0
config/src/test/java/org/springframework/security/config/annotation/web/configurers/TestMockHttpServletMappings.java

@@ -0,0 +1,46 @@
+/*
+ * Copyright 2002-2023 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.configurers;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.MappingMatch;
+
+import org.springframework.mock.web.MockHttpServletMapping;
+
+final class TestMockHttpServletMappings {
+
+	private TestMockHttpServletMappings() {
+
+	}
+
+	static MockHttpServletMapping extension(HttpServletRequest request, String extension) {
+		String uri = request.getRequestURI();
+		String matchValue = uri.substring(0, uri.lastIndexOf(extension));
+		return new MockHttpServletMapping(matchValue, "*" + extension, "extension", MappingMatch.EXTENSION);
+	}
+
+	static MockHttpServletMapping path(HttpServletRequest request, String path) {
+		String uri = request.getRequestURI();
+		String matchValue = uri.substring(path.length());
+		return new MockHttpServletMapping(matchValue, path + "/*", "path", MappingMatch.PATH);
+	}
+
+	static MockHttpServletMapping defaultMapping() {
+		return new MockHttpServletMapping("", "/", "default", MappingMatch.DEFAULT);
+	}
+
+}

+ 118 - 32
docs/modules/ROOT/pages/servlet/authorization/authorize-http-requests.adoc

@@ -571,70 +571,156 @@ http {
 ----
 ====
 
+[[match-by-servlet-path]]
+[[mvc-not-default-servlet]]
 [[match-by-mvc]]
-=== Using an MvcRequestMatcher
+=== Matching by Servlet Pattern
 
-Generally speaking, you can use `requestMatchers(String)` as demonstrated above.
+Generally speaking, you can use `requestMatchers(String...)` and `requestMatchers(HttpMethod, String...)` as demonstrated above.
 
 However, if you map Spring MVC to a different servlet path, then you need to account for that in your security configuration.
 
-For example, if Spring MVC is mapped to `/spring-mvc` instead of `/` (the default), then you may have an endpoint like `/spring-mvc/my/controller` that you want to authorize.
+For example, if Spring MVC is mapped to `/mvc` instead of `/` (the default), then you may have an endpoint like `/mvc/my/controller` that you want to authorize.
 
-You need to use `MvcRequestMatcher` to split the servlet path and the controller path in your configuration like so:
+If you have multiple servlets, and `DispatcherServlet` is mapped in this way, you'll see an error that's something like this:
+
+[source,bash]
+----
+This method cannot decide whether these patterns are Spring MVC patterns or not
+
+...
+
+For your reference, here is your servlet configuration: {default=[/], dispatcherServlet=[/mvc/*]}
+
+To address this, you need to specify the servlet path or pattern for each endpoint.
+You can use .forServletPattern in conjunction with requestMatchers do to this
+----
+
+You can use `.forServletPattern` (or construct your own `MvcRequestMatcher` instance) to split the servlet path and the controller path in your configuration, like so:
 
 .Match by MvcRequestMatcher
 ====
 .Java
 [source,java,role="primary"]
 ----
-@Bean
-MvcRequestMatcher.Builder mvc(HandlerMappingIntrospector introspector) {
-	return new MvcRequestMatcher.Builder(introspector).servletPath("/spring-mvc");
-}
-
 @Bean
 SecurityFilterChain appEndpoints(HttpSecurity http, MvcRequestMatcher.Builder mvc) {
 	http
         .authorizeHttpRequests((authorize) -> authorize
-            .requestMatchers(mvc.pattern("/my/controller/**")).hasAuthority("controller")
-            .anyRequest().authenticated()
+            .forServletPattern("/mvc/*", (mvc) -> mvc
+                .requestMatchers("/my/resource/**").hasAuthority("resource:read")
+                .anyRequest().authenticated()
+            )
         );
 
 	return http.build();
 }
 ----
+====
 
-.Kotlin
-[source,kotlin,role="secondary"]
+where `/mvc/*` is the matching pattern in your servlet configuration listed in the error message.
+
+This need can arise in at least two different ways:
+
+* If you use the `spring.mvc.servlet.path` Boot property to change the default path (`/`) to something else
+* If you register more than one Spring MVC `DispatcherServlet` (thus requiring that one of them not be the default servlet)
+
+Note that when either of these cases come up, all URIs need to be fully-qualified as above.
+
+For example, consider a more sophisticated setup where you have Spring MVC resources mapped to `/mvc/*` and Spring Boot H2 Console mapped to `/h2-console/*`.
+In that case, each URI can be made absolute, listing the servlet path like so:
+
+.Match by Servlet Path
+====
+.Java
+[source,java,role="primary"]
 ----
 @Bean
-fun mvc(introspector: HandlerMappingIntrospector): MvcRequestMatcher.Builder =
-    MvcRequestMatcher.Builder(introspector).servletPath("/spring-mvc");
+SecurityFilterChain appSecurity(HttpSecurity http) throws Exception {
+    http
+        .authorizeHttpRequests((authorize) -> authorize
+            .forServletPattern("/mvc/*", (mvc) -> mvc
+                .requestMatchers("/my/resource/**").hasAuthority("resource:read")
+            )
+            .forServletPattern("/h2-console/*", (h2) -> h2
+                .anyRequest().hasAuthority("h2")
+            )
+        )
+    // ...
+}
+----
+====
+
+Alternatively, you can do one of three things to remove the need to disambiguate:
+
+1. Always deploy `DispatcherServlet` to `/` (the default behavior)
++
+When `DispatcherServlet` is mapped to `/`, it's clear that all the URIs supplied in `requestMatchers(String)` are absolute URIs.
+Because of that, there is no ambiguity when interpreting them.
++
+2. Remove all other servlets
++
+When there is only `DispatcherServlet`, it's clear that all the URIs supplied in `requestMatchers(String)` are relative to the Spring MVC configuration.
+Because of that, there is no ambiguity when interpreting them.
+
+At times, servlet containers add other servlets by default that you aren't actually using.
+So, if these aren't needed, remove them, bringing you down to just `DispatcherServlet`.
++
+3. Create an `HttpRequestHandler` so that `DispatcherServlet` dispatches to your servlets instead of your servlet container.
++
+If you are deploying Spring MVC to a separate path to allow your container to serve static resources, consider instead {spring-framework-reference-url}web/webmvc/mvc-config/default-servlet-handler.html#page-title[notifying Spring MVC about this].
+Or, if you have a custom servlet, publishing {spring-framework-api-url}org/springframework/web/servlet/mvc/HttpRequestHandlerAdapter.html[a custom `HttpRequestHandler` bean within {spring-framework-api-url}org/springframework/web/servlet/DispatcherServlet.html[the `DispatcherServlet` configuration] instead.
++
+
+=== Matching by the Default Servlet
+
+You can also match more generally by the matching pattern specified in your servlet configuration.
+
+For example, to match the default servlet (whichever servlet is mapped to `/`), use `forServletPattern` like so:
 
+.Match by the Default Servlet
+====
+.Java
+[source,java,role="primary"]
+----
 @Bean
-fun appEndpoints(http: HttpSecurity, mvc: MvcRequestMatcher.Builder): SecurityFilterChain =
-    http {
-        authorizeHttpRequests {
-            authorize(mvc.pattern("/my/controller/**"), hasAuthority("controller"))
-            authorize(anyRequest, authenticated)
-        }
-    }
+SecurityFilterChain appSecurity(HttpSecurity http) throws Exception {
+    http
+        .authorizeHttpRequests((authorize) -> authorize
+            .forServletPattern("/", (root) -> root
+                .requestMatchers("/my/resource/**").hasAuthority("resource:read")
+            )
+        )
+    // ...
+}
 ----
+====
 
-.Xml
-[source,xml,role="secondary"]
+Such will match on requests that the servlet container matches to your default servlet that start with the URI `/my/resource`.
+
+=== Matching by an Extension Servlet
+
+Or, to match to an extension servlet (like a servlet mapped to `*.jsp`), use `forServletPattern` as follows:
+
+.Match by an Extension Servlet
+====
+.Java
+[source,java,role="primary"]
 ----
-<http>
-    <intercept-url servlet-path="/spring-mvc" pattern="/my/controller/**" access="hasAuthority('controller')"/>
-    <intercept-url pattern="/**" access="authenticated"/>
-</http>
+@Bean
+SecurityFilterChain appSecurity(HttpSecurity http) throws Exception {
+    http
+        .authorizeHttpRequests((authorize) -> authorize
+            .forServletPattern("*.jsp", (jsp) -> jsp
+                .requestMatchers("/my/resource/**").hasAuthority("resource:read")
+            )
+        )
+    // ...
+}
 ----
 ====
 
-This need can arise in at least two different ways:
-
-* If you use the `spring.mvc.servlet.path` Boot property to change the default path (`/`) to something else
-* If you register more than one Spring MVC `DispatcherServlet` (thus requiring that one of them not be the default path)
+Such will match on requests that the servlet container matches to your `*.jsp` servlet that start with the URI `/my/resource` (for example a request like `/my/resource/page.jsp`).
 
 [[match-by-custom]]
 === Using a Custom Matcher