Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RegexRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcherFactory;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.web.context.WebApplicationContext;
Expand All @@ -73,6 +74,8 @@ public abstract class AbstractRequestMatcherRegistry<C> {

private static final RequestMatcher ANY_REQUEST = AnyRequestMatcher.INSTANCE;

private final RequestMatcherFactory requestMatcherFactory = new DefaultRequestMatcherFactory();

private ApplicationContext context;

private boolean anyRequestConfigured = false;
Expand Down Expand Up @@ -216,13 +219,9 @@ public C requestMatchers(HttpMethod method, String... patterns) {
if (servletContext == null) {
return requestMatchers(RequestMatchers.antMatchersAsArray(method, patterns));
}
List<RequestMatcher> matchers = new ArrayList<>();
for (String pattern : patterns) {
AntPathRequestMatcher ant = new AntPathRequestMatcher(pattern, (method != null) ? method.name() : null);
MvcRequestMatcher mvc = createMvcMatchers(method, pattern).get(0);
matchers.add(new DeferredRequestMatcher((c) -> resolve(ant, mvc, c), mvc, ant));
}
return requestMatchers(matchers.toArray(new RequestMatcher[0]));
RequestMatcherFactory builder = context.getBeanProvider(RequestMatcherFactory.class)
.getIfUnique(() -> this.requestMatcherFactory);
return requestMatchers(builder.requestMatchers(method, patterns));
}

private boolean anyPathsDontStartWithLeadingSlash(String... patterns) {
Expand Down Expand Up @@ -473,6 +472,17 @@ static List<RequestMatcher> regexMatchers(String... regexPatterns) {

}

class DefaultRequestMatcherFactory implements RequestMatcherFactory {

@Override
public RequestMatcher requestMatcher(HttpMethod method, String pattern) {
AntPathRequestMatcher ant = new AntPathRequestMatcher(pattern, (method != null) ? method.name() : null);
MvcRequestMatcher mvc = createMvcMatchers(method, pattern).get(0);
return new DeferredRequestMatcher((c) -> resolve(ant, mvc, c), mvc, ant);
}

}

static class DeferredRequestMatcher implements RequestMatcher {

final Function<ServletContext, RequestMatcher> requestMatcherFactory;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.expression.BeanResolver;
import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults;
Expand All @@ -54,6 +55,7 @@
import org.springframework.security.web.method.annotation.CsrfTokenArgumentResolver;
import org.springframework.security.web.method.annotation.CurrentSecurityContextArgumentResolver;
import org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcherFactory;
import org.springframework.web.filter.CompositeFilter;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
Expand Down Expand Up @@ -175,6 +177,12 @@ public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) t
};
}

@Bean
@Scope("prototype")
MvcRequestMatcherFactory.Builder mvcRequestMatcherFactoryBuilder(HandlerMappingIntrospector introspector) {
return MvcRequestMatcherFactory.builder(introspector);
}

/**
* {@link FactoryBean} to defer creation of
* {@link HandlerMappingIntrospector#createCacheFilter()}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.ApplicationContext;
Expand All @@ -42,6 +43,7 @@
import org.springframework.security.web.util.matcher.DispatcherTypeRequestMatcher;
import org.springframework.security.web.util.matcher.RegexRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcherFactory;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
Expand Down Expand Up @@ -87,6 +89,13 @@ public void setUp() {
given(given).willReturn(postProcessors);
given(postProcessors.getObject()).willReturn(NO_OP_OBJECT_POST_PROCESSOR);
given(this.context.getServletContext()).willReturn(MockServletContext.mvc());
ObjectProvider<RequestMatcherFactory> requestMatcherFactories = new ObjectProvider<>() {
@Override
public RequestMatcherFactory getObject() throws BeansException {
return AbstractRequestMatcherRegistryTests.this.matcherRegistry.new DefaultRequestMatcherFactory();
}
};
given(this.context.getBeanProvider(RequestMatcherFactory.class)).willReturn(requestMatcherFactories);
this.matcherRegistry.setApplicationContext(this.context);
mockMvcIntrospector(true);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@
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.servlet.util.matcher.MvcRequestMatcherFactory;
import org.springframework.security.web.util.matcher.RequestMatcherFactory;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.RequestPostProcessor;
Expand All @@ -72,6 +74,7 @@
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;

Expand Down Expand Up @@ -667,6 +670,19 @@ public void getWhenExcludeAuthorizationObservationsThenUnobserved() throws Excep
verifyNoInteractions(handler);
}

@Test
public void requestMatchersWhenMultipleDispatcherServletsAndPathBeanThenAllows() throws Exception {
this.spring.register(MvcRequestMatcherFactoryConfig.class, BasicController.class)
.postProcessor((context) -> context.getServletContext()
.addServlet("otherDispatcherServlet", DispatcherServlet.class)
.addMapping("/mvc"))
.autowire();
this.mvc.perform(get("/mvc/path").servletPath("/mvc").with(user("user"))).andExpect(status().isOk());
this.mvc.perform(get("/mvc/path").servletPath("/mvc").with(user("user").roles("DENIED")))
.andExpect(status().isForbidden());
this.mvc.perform(get("/path").with(user("user"))).andExpect(status().isForbidden());
}

@Configuration
@EnableWebSecurity
static class GrantedAuthorityDefaultHasRoleConfig {
Expand Down Expand Up @@ -1262,6 +1278,10 @@ void rootGet() {
void rootPost() {
}

@GetMapping("/path")
void path() {
}

}

@Configuration
Expand Down Expand Up @@ -1317,4 +1337,23 @@ SecurityObservationSettings observabilityDefaults() {

}

@Configuration
@EnableWebSecurity
@EnableWebMvc
static class MvcRequestMatcherFactoryConfig {

@Bean
RequestMatcherFactory servletPath(MvcRequestMatcherFactory.Builder builder) {
return builder.servletPath("/mvc").build();
}

@Bean
SecurityFilterChain security(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((authorize) -> authorize.requestMatchers("/path").hasRole("USER"))
.httpBasic(withDefaults());
return http.build();
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -640,11 +640,120 @@ Xml::
----
======

[[conditions-for-servlet-path-matching]]
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)

=== Using a `RequestMatcherFactory`

You can reduce the boilerplate of constructing several `MvcRequestMatcher` instances by providing a single instance of `RequestMatcherFactory`.

For example, if all of your requests in `requestMatcher(String)` are MVC requests, then you can do:

[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
RequestMatcherFactory allRequestsAreMvc(HandlerMappingIntrospector introspector) {
MvcRequestMatcher.Builder mvc = new MvcRequestMatcher.Builder(introspector).servletPath("/my-servlet-path");
return mvc::pattern;
}
----

Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean fun allRequestsAreMvc(introspector: HandlerMappingIntrospector?): RequestMatcherFactory {
var mvc = MvcRequestMatcher.Builder(introspector).servletPath("/my-servlet-path")
return mvc::pattern
}
----
======

Spring Security will use this builder for all request matchers specified as a `String`.

[TIP]
====
Often the only non-MVC requests that there are in a Spring Boot application are those to static resources like `/css", '/js', and 'favicon.ico`.
====

You can permit these by using Spring Boot's `RequestMatchers` static factory like so:

[tabs]
======
Java::
+
[source,java]
----
@Bean
SecurityFilterChain security(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.requestMatchers("/my", "/mvc", "/requests").hasAuthority("app")
)
}
----

Kotlin::
+
[source,kotlin,role="secondary"]
----
http {
authorizeHttpRequests {
authorize(PathRequest.toStaticResources().atCommonLocations(), permitAll)
authorize("/my", hasAuthority("app"))
authorize("/mvc", hasAuthority("app"))
authorize("/requests", hasAuthority("app"))
}
}
----
======

Since `atCommonLocations` returns instances of `RequestMatcher`, this technique allows you to publish an MVC-based `RequestMatcherFactory` for the rest.

In the event that <<conditions-for-servlet-path-matching, the absolute path would be ambiguous>>, you can publish an `MvcDelegatingRequestMatcherFactory` instance instead:

[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
RequestMatcherFactory allRequestsAreMvc(MvcRequestMatcherFactory.Builder builder) {
return builder.servletPath("/my-mvc-servlet-path").build();
}
----

Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun allRequestsAreMvc(builder: MvcRequestMatcherFactory.Builder?): RequestMatcherBuilder {
return builder.servletPath("/my-mvc-servlet-path").build()
}
----
======

This produces matchers that check first if the request is an MVC request; if it is, use an `MvcRequestMatcher` and otherwise use an `AntPathRequestMatcher`.

[NOTE]
====
The reason this `RequestMatcherFactory` is not used by default is because of potential ambiguities in the meaning of given `String` patterns.
For example, consider a servlet mapped to `/example` and a Spring MVC endpoint mapped to `/mvc-servlet-path/example` where `/mvc-servlet-path` is the servlet path for MVC endpoints.
In this case, it's unclear whether by `requestMatchers("/example")` you mean to secure `/example`` or `/mvc-servlet-path/example`.

Publishing any `RequestMatcherFactory` indicates that you will handle these ambiguous situations, should they arise.
====

[[match-by-custom]]
=== Using a Custom Matcher

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
*/
public class MvcRequestMatcher implements RequestMatcher, RequestVariablesExtractor {

private final DefaultMatcher defaultMatcher = new DefaultMatcher();
private RequestMatcher defaultMatcher = new DefaultMatcher();

private final HandlerMappingIntrospector introspector;

Expand Down Expand Up @@ -130,6 +130,16 @@ protected final String getServletPath() {
return this.servletPath;
}

/**
* The matcher that this should fall back on in the event that the request isn't
* recognized by Spring MVC
* @param defaultMatcher the default matcher to use
* @since 6.4
*/
public void setDefaultMatcher(RequestMatcher defaultMatcher) {
this.defaultMatcher = defaultMatcher;
}

@Override
public boolean equals(Object o) {
if (this == o) {
Expand Down
Loading
Loading