Simplify Java Configuration RequestMatcher Usage

If Spring MVC is present in the classpath, use MvcRequestMatcher by default. This commit also adds a new securityMatcher method in HttpSecurity

Closes gh-11347
Closes gh-9159
This commit is contained in:
Marcus Da Coregio 2022-09-21 10:09:35 -03:00 committed by Marcus Hert Da Coregio
parent bf59d7c374
commit 039e0328e1
18 changed files with 1395 additions and 46 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2022 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.
@ -35,6 +35,7 @@ import org.springframework.security.web.util.matcher.DispatcherTypeRequestMatche
import org.springframework.security.web.util.matcher.RegexRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
/**
@ -50,12 +51,21 @@ public abstract class AbstractRequestMatcherRegistry<C> {
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;
private static final RequestMatcher ANY_REQUEST = AnyRequestMatcher.INSTANCE;
private ApplicationContext context;
private boolean anyRequestConfigured = false;
static {
mvcPresent = ClassUtils.isPresent(HANDLER_MAPPING_INTROSPECTOR,
AbstractRequestMatcherRegistry.class.getClassLoader());
}
protected final void setApplicationContext(ApplicationContext context) {
this.context = context;
}
@ -85,7 +95,9 @@ public abstract class AbstractRequestMatcherRegistry<C> {
* instances.
* @param method the {@link HttpMethod} to use for any {@link HttpMethod}.
* @return the object that is chained after creating the {@link RequestMatcher}
* @deprecated use {@link #requestMatchers(HttpMethod)} instead
*/
@Deprecated
public C antMatchers(HttpMethod method) {
return antMatchers(method, "/**");
}
@ -99,7 +111,9 @@ public abstract class AbstractRequestMatcherRegistry<C> {
* @param antPatterns the ant patterns to create. If {@code null} or empty, then
* matches on nothing.
* @return the object that is chained after creating the {@link RequestMatcher}
* @deprecated use {@link #requestMatchers(HttpMethod, String...)} instead
*/
@Deprecated
public C antMatchers(HttpMethod method, String... antPatterns) {
Assert.state(!this.anyRequestConfigured, "Can't configure antMatchers after anyRequest");
return chainRequestMatchers(RequestMatchers.antMatchers(method, antPatterns));
@ -112,7 +126,9 @@ public abstract class AbstractRequestMatcherRegistry<C> {
* @param antPatterns the ant patterns to create
* {@link org.springframework.security.web.util.matcher.AntPathRequestMatcher} from
* @return the object that is chained after creating the {@link RequestMatcher}
* @deprecated use {@link #requestMatchers(String...)} instead
*/
@Deprecated
public C antMatchers(String... antPatterns) {
Assert.state(!this.anyRequestConfigured, "Can't configure antMatchers after anyRequest");
return chainRequestMatchers(RequestMatchers.antMatchers(antPatterns));
@ -132,7 +148,9 @@ public abstract class AbstractRequestMatcherRegistry<C> {
* @param mvcPatterns the patterns to match on. The rules for matching are defined by
* Spring MVC
* @return the object that is chained after creating the {@link RequestMatcher}.
* @deprecated use {@link #requestMatchers(String...)} instead
*/
@Deprecated
public abstract C mvcMatchers(String... mvcPatterns);
/**
@ -150,7 +168,9 @@ public abstract class AbstractRequestMatcherRegistry<C> {
* @param mvcPatterns the patterns to match on. The rules for matching are defined by
* Spring MVC
* @return the object that is chained after creating the {@link RequestMatcher}.
* @deprecated use {@link #requestMatchers(HttpMethod, String...)} instead
*/
@Deprecated
public abstract C mvcMatchers(HttpMethod method, String... mvcPatterns);
/**
@ -190,7 +210,10 @@ public abstract class AbstractRequestMatcherRegistry<C> {
* @param regexPatterns the regular expressions to create
* {@link org.springframework.security.web.util.matcher.RegexRequestMatcher} from
* @return the object that is chained after creating the {@link RequestMatcher}
* @deprecated use {@link #requestMatchers(RequestMatcher...)} with a
* {@link RegexRequestMatcher} instead
*/
@Deprecated
public C regexMatchers(HttpMethod method, String... regexPatterns) {
Assert.state(!this.anyRequestConfigured, "Can't configure regexMatchers after anyRequest");
return chainRequestMatchers(RequestMatchers.regexMatchers(method, regexPatterns));
@ -203,7 +226,10 @@ public abstract class AbstractRequestMatcherRegistry<C> {
* @param regexPatterns the regular expressions to create
* {@link org.springframework.security.web.util.matcher.RegexRequestMatcher} from
* @return the object that is chained after creating the {@link RequestMatcher}
* @deprecated use {@link #requestMatchers(RequestMatcher...)} with a
* {@link RegexRequestMatcher} instead
*/
@Deprecated
public C regexMatchers(String... regexPatterns) {
Assert.state(!this.anyRequestConfigured, "Can't configure regexMatchers after anyRequest");
return chainRequestMatchers(RequestMatchers.regexMatchers(regexPatterns));
@ -250,6 +276,81 @@ public abstract class AbstractRequestMatcherRegistry<C> {
return chainRequestMatchers(Arrays.asList(requestMatchers));
}
/**
* <p>
* If the {@link HandlerMappingIntrospector} is available in the classpath, maps to an
* {@link MvcRequestMatcher} that also specifies a specific {@link HttpMethod} to
* match on. This matcher will use the same rules that Spring MVC uses for matching.
* For example, often times a mapping of the path "/path" will match on "/path",
* "/path/", "/path.html", etc. If the {@link HandlerMappingIntrospector} is not
* available, maps to an {@link AntPathRequestMatcher}.
* </p>
* <p>
* If a specific {@link RequestMatcher} must be specified, use
* {@link #requestMatchers(RequestMatcher...)} instead
* </p>
* @param method the {@link HttpMethod} to use or {@code null} for any
* {@link HttpMethod}.
* @param patterns the patterns to match on. The rules for matching are defined by
* Spring MVC if {@link MvcRequestMatcher} is used
* @return the object that is chained after creating the {@link RequestMatcher}.
* @since 5.8
*/
public C requestMatchers(HttpMethod method, String... patterns) {
List<RequestMatcher> matchers = new ArrayList<>();
if (mvcPresent) {
matchers.addAll(createMvcMatchers(method, patterns));
}
else {
matchers.addAll(RequestMatchers.antMatchers(method, patterns));
}
return requestMatchers(matchers.toArray(new RequestMatcher[0]));
}
/**
* <p>
* If the {@link HandlerMappingIntrospector} is available in the classpath, maps to an
* {@link MvcRequestMatcher} that does not care which {@link HttpMethod} is used. This
* matcher will use the same rules that Spring MVC uses for matching. For example,
* often times a mapping of the path "/path" will match on "/path", "/path/",
* "/path.html", etc. If the {@link HandlerMappingIntrospector} is not available, maps
* to an {@link AntPathRequestMatcher}.
* </p>
* <p>
* If a specific {@link RequestMatcher} must be specified, use
* {@link #requestMatchers(RequestMatcher...)} instead
* </p>
* @param patterns the patterns to match on. The rules for matching are defined by
* Spring MVC if {@link MvcRequestMatcher} is used
* @return the object that is chained after creating the {@link RequestMatcher}.
* @since 5.8
*/
public C requestMatchers(String... patterns) {
return requestMatchers(null, patterns);
}
/**
* <p>
* If the {@link HandlerMappingIntrospector} is available in the classpath, maps to an
* {@link MvcRequestMatcher} that matches on a specific {@link HttpMethod}. This
* matcher will use the same rules that Spring MVC uses for matching. For example,
* often times a mapping of the path "/path" will match on "/path", "/path/",
* "/path.html", etc. If the {@link HandlerMappingIntrospector} is not available, maps
* to an {@link AntPathRequestMatcher}.
* </p>
* <p>
* If a specific {@link RequestMatcher} must be specified, use
* {@link #requestMatchers(RequestMatcher...)} instead
* </p>
* @param method the {@link HttpMethod} to use or {@code null} for any
* {@link HttpMethod}.
* @return the object that is chained after creating the {@link RequestMatcher}.
* @since 5.8
*/
public C requestMatchers(HttpMethod method) {
return requestMatchers(method, "/**");
}
/**
* Subclasses should implement this method for returning the object that is chained to
* the creation of the {@link RequestMatcher} instances.

View File

@ -28,6 +28,7 @@ import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.core.OrderComparator;
import org.springframework.core.Ordered;
@ -92,6 +93,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.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
@ -117,7 +119,7 @@ import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
*
* &#064;Bean
* public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
* http.authorizeRequests().antMatchers(&quot;/**&quot;).hasRole(&quot;USER&quot;).and().formLogin();
* http.authorizeHttpRequests().requestMatchers(&quot;/**&quot;).hasRole(&quot;USER&quot;).and().formLogin();
* return http.build();
* }
*
@ -141,6 +143,12 @@ import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<DefaultSecurityFilterChain, HttpSecurity>
implements SecurityBuilder<DefaultSecurityFilterChain>, HttpSecurityBuilder<HttpSecurity> {
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;
private final RequestMatcherConfigurer requestMatcherConfigurer;
private List<OrderedFilter> filters = new ArrayList<>();
@ -151,6 +159,10 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<Defaul
private AuthenticationManager authenticationManager;
static {
mvcPresent = ClassUtils.isPresent(HANDLER_MAPPING_INTROSPECTOR, HttpSecurity.class.getClassLoader());
}
/**
* Creates a new instance
* @param objectPostProcessor the {@link ObjectPostProcessor} that should be used
@ -3415,7 +3427,9 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<Defaul
* }
* </pre>
* @return the {@link RequestMatcherConfigurer} for further customizations
* @deprecated use {@link #securityMatchers()} instead
*/
@Deprecated
public RequestMatcherConfigurer requestMatchers() {
return this.requestMatcherConfigurer;
}
@ -3547,7 +3561,9 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<Defaul
* @param requestMatcherCustomizer the {@link Customizer} to provide more options for
* the {@link RequestMatcherConfigurer}
* @return the {@link HttpSecurity} for further customizations
* @deprecated use {@link #securityMatchers(Customizer)} instead
*/
@Deprecated
public HttpSecurity requestMatchers(Customizer<RequestMatcherConfigurer> requestMatcherCustomizer) {
requestMatcherCustomizer.customize(this.requestMatcherConfigurer);
return HttpSecurity.this;
@ -3567,15 +3583,318 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<Defaul
* @param requestMatcher the {@link RequestMatcher} to use (i.e. new
* AntPathRequestMatcher("/admin/**","GET") )
* @return the {@link HttpSecurity} for further customizations
* @deprecated use {@link #securityMatcher(RequestMatcher)} instead
* @see #requestMatchers()
* @see #antMatcher(String)
* @see #regexMatcher(String)
*/
@Deprecated
public HttpSecurity requestMatcher(RequestMatcher requestMatcher) {
this.requestMatcher = requestMatcher;
return this;
}
/**
* Allows specifying which {@link HttpServletRequest} instances this
* {@link HttpSecurity} will be invoked on. This method allows for easily invoking the
* {@link HttpSecurity} for multiple different {@link RequestMatcher} instances. If
* only a single {@link RequestMatcher} is necessary consider using
* {@link #securityMatcher(String)}, or {@link #securityMatcher(RequestMatcher)}.
*
* <p>
* Invoking {@link #securityMatchers()} will not override previous invocations of
* {@link #securityMatchers()}}, {@link #securityMatchers(Customizer)}
* {@link #securityMatcher(String)} and {@link #securityMatcher(RequestMatcher)}
* </p>
*
* <h3>Example Configurations</h3>
*
* The following configuration enables the {@link HttpSecurity} for URLs that begin
* with "/api/" or "/oauth/".
*
* <pre>
* &#064;Configuration
* &#064;EnableWebSecurity
* public class RequestMatchersSecurityConfig {
*
* &#064;Bean
* public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
* http
* .securityMatchers((matchers) -&gt; matchers
* .requestMatchers(&quot;/api/**&quot;, &quot;/oauth/**&quot;)
* )
* .authorizeHttpRequests((authorize) -&gt; authorize
* anyRequest().hasRole(&quot;USER&quot;)
* )
* .httpBasic(withDefaults());
* return http.build();
* }
*
* &#064;Bean
* public UserDetailsService userDetailsService() {
* UserDetails user = User.withDefaultPasswordEncoder()
* .username(&quot;user&quot;)
* .password(&quot;password&quot;)
* .roles(&quot;USER&quot;)
* .build();
* return new InMemoryUserDetailsManager(user);
* }
* }
* </pre>
*
* The configuration below is the same as the previous configuration.
*
* <pre>
* &#064;Configuration
* &#064;EnableWebSecurity
* public class RequestMatchersSecurityConfig {
*
* &#064;Bean
* public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
* http
* .securityMatchers((matchers) -&gt; matchers
* .requestMatchers(&quot;/api/**&quot;)
* .requestMatchers(&quot;/oauth/**&quot;)
* )
* .authorizeHttpRequests((authorize) -&gt; authorize
* anyRequest().hasRole(&quot;USER&quot;)
* )
* .httpBasic(withDefaults());
* return http.build();
* }
*
* &#064;Bean
* public UserDetailsService userDetailsService() {
* UserDetails user = User.withDefaultPasswordEncoder()
* .username(&quot;user&quot;)
* .password(&quot;password&quot;)
* .roles(&quot;USER&quot;)
* .build();
* return new InMemoryUserDetailsManager(user);
* }
* }
* </pre>
*
* The configuration below is also the same as the above configuration.
*
* <pre>
* &#064;Configuration
* &#064;EnableWebSecurity
* public class RequestMatchersSecurityConfig {
*
* &#064;Bean
* public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
* http
* .securityMatchers((matchers) -&gt; matchers
* .requestMatchers(&quot;/api/**&quot;)
* )
* .securityMatchers((matchers) -&gt; matchers
* .requestMatchers(&quot;/oauth/**&quot;)
* )
* .authorizeHttpRequests((authorize) -&gt; authorize
* anyRequest().hasRole(&quot;USER&quot;)
* )
* .httpBasic(withDefaults());
* return http.build();
* }
*
* &#064;Bean
* public UserDetailsService userDetailsService() {
* UserDetails user = User.withDefaultPasswordEncoder()
* .username(&quot;user&quot;)
* .password(&quot;password&quot;)
* .roles(&quot;USER&quot;)
* .build();
* return new InMemoryUserDetailsManager(user);
* }
* }
* </pre>
* @return the {@link RequestMatcherConfigurer} for further customizations
*/
public RequestMatcherConfigurer securityMatchers() {
return this.requestMatcherConfigurer;
}
/**
* Allows specifying which {@link HttpServletRequest} instances this
* {@link HttpSecurity} will be invoked on. This method allows for easily invoking the
* {@link HttpSecurity} for multiple different {@link RequestMatcher} instances. If
* only a single {@link RequestMatcher} is necessary consider using
* {@link #securityMatcher(String)}, or {@link #securityMatcher(RequestMatcher)}.
*
* <p>
* Invoking {@link #securityMatchers(Customizer)} will not override previous
* invocations of {@link #securityMatchers()}}, {@link #securityMatchers(Customizer)}
* {@link #securityMatcher(String)} and {@link #securityMatcher(RequestMatcher)}
* </p>
*
* <h3>Example Configurations</h3>
*
* The following configuration enables the {@link HttpSecurity} for URLs that begin
* with "/api/" or "/oauth/".
*
* <pre>
* &#064;Configuration
* &#064;EnableWebSecurity
* public class RequestMatchersSecurityConfig {
*
* &#064;Bean
* public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
* http
* .securityMatchers((matchers) -&gt; matchers
* .requestMatchers(&quot;/api/**&quot;, &quot;/oauth/**&quot;)
* )
* .authorizeHttpRequests((authorize) -&gt; authorize
* .anyRequest().hasRole(&quot;USER&quot;)
* )
* .httpBasic(withDefaults());
* return http.build();
* }
*
* &#064;Bean
* public UserDetailsService userDetailsService() {
* UserDetails user = User.withDefaultPasswordEncoder()
* .username(&quot;user&quot;)
* .password(&quot;password&quot;)
* .roles(&quot;USER&quot;)
* .build();
* return new InMemoryUserDetailsManager(user);
* }
* }
* </pre>
*
* The configuration below is the same as the previous configuration.
*
* <pre>
* &#064;Configuration
* &#064;EnableWebSecurity
* public class RequestMatchersSecurityConfig {
*
* &#064;Bean
* public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
* http
* .securityMatchers((matchers) -&gt; matchers
* .requestMatchers(&quot;/api/**&quot;)
* .requestMatchers(&quot;/oauth/**&quot;)
* )
* .authorizeHttpRequests((authorize) -&gt; authorize
* .anyRequest().hasRole(&quot;USER&quot;)
* )
* .httpBasic(withDefaults());
* return http.build();
* }
*
* &#064;Bean
* public UserDetailsService userDetailsService() {
* UserDetails user = User.withDefaultPasswordEncoder()
* .username(&quot;user&quot;)
* .password(&quot;password&quot;)
* .roles(&quot;USER&quot;)
* .build();
* return new InMemoryUserDetailsManager(user);
* }
* }
* </pre>
*
* The configuration below is also the same as the above configuration.
*
* <pre>
* &#064;Configuration
* &#064;EnableWebSecurity
* public class RequestMatchersSecurityConfig {
*
* &#064;Bean
* public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
* http
* .securityMatchers((matchers) -&gt; matchers
* .requestMatchers(&quot;/api/**&quot;)
* )
* .securityMatchers((matchers) -&gt; matchers
* .requestMatchers(&quot;/oauth/**&quot;)
* )
* .authorizeHttpRequests((authorize) -&gt; authorize
* .anyRequest().hasRole(&quot;USER&quot;)
* )
* .httpBasic(withDefaults());
* return http.build();
* }
*
* &#064;Bean
* public UserDetailsService userDetailsService() {
* UserDetails user = User.withDefaultPasswordEncoder()
* .username(&quot;user&quot;)
* .password(&quot;password&quot;)
* .roles(&quot;USER&quot;)
* .build();
* return new InMemoryUserDetailsManager(user);
* }
* }
* </pre>
* @param requestMatcherCustomizer the {@link Customizer} to provide more options for
* the {@link RequestMatcherConfigurer}
* @return the {@link HttpSecurity} for further customizations
*/
public HttpSecurity securityMatchers(Customizer<RequestMatcherConfigurer> requestMatcherCustomizer) {
requestMatcherCustomizer.customize(this.requestMatcherConfigurer);
return HttpSecurity.this;
}
/**
* Allows configuring the {@link HttpSecurity} to only be invoked when matching the
* provided {@link RequestMatcher}. If more advanced configuration is necessary,
* consider using {@link #securityMatchers(Customizer)} ()}.
*
* <p>
* Invoking {@link #securityMatcher(RequestMatcher)} will override previous
* invocations of {@link #requestMatchers()}, {@link #mvcMatcher(String)},
* {@link #antMatcher(String)}, {@link #regexMatcher(String)},
* {@link #requestMatcher(RequestMatcher)}, {@link #securityMatchers(Customizer)},
* {@link #securityMatchers()} and {@link #securityMatcher(String)}
* </p>
* @param requestMatcher the {@link RequestMatcher} to use (i.e. new
* AntPathRequestMatcher("/admin/**","GET") )
* @return the {@link HttpSecurity} for further customizations
* @see #securityMatcher(String)
*/
public HttpSecurity securityMatcher(RequestMatcher requestMatcher) {
this.requestMatcher = requestMatcher;
return this;
}
/**
* Allows configuring the {@link HttpSecurity} to only be invoked when matching the
* provided pattern. This method creates a {@link MvcRequestMatcher} if Spring MVC is
* in the classpath or creates an {@link AntPathRequestMatcher} if not. If more
* advanced configuration is necessary, consider using
* {@link #securityMatchers(Customizer)} or {@link #securityMatcher(RequestMatcher)}.
*
* <p>
* Invoking {@link #securityMatcher(String)} will override previous invocations of
* {@link #mvcMatcher(String)}}, {@link #requestMatchers()},
* {@link #antMatcher(String)}, {@link #regexMatcher(String)}, and
* {@link #requestMatcher(RequestMatcher)}.
* </p>
* @param pattern the pattern to match on (i.e. "/admin/**")
* @return the {@link HttpSecurity} for further customizations
* @see AntPathRequestMatcher
* @see MvcRequestMatcher
*/
public HttpSecurity securityMatcher(String pattern) {
if (!mvcPresent) {
this.requestMatcher = new AntPathRequestMatcher(pattern);
return this;
}
if (!getContext().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.");
}
HandlerMappingIntrospector introspector = getContext().getBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME,
HandlerMappingIntrospector.class);
this.requestMatcher = new MvcRequestMatcher(introspector, pattern);
return this;
}
/**
* Allows configuring the {@link HttpSecurity} to only be invoked when matching the
* provided ant pattern. If more advanced configuration is necessary, consider using
@ -3589,8 +3908,10 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<Defaul
* </p>
* @param antPattern the Ant Pattern to match on (i.e. "/admin/**")
* @return the {@link HttpSecurity} for further customizations
* @deprecated use {@link #securityMatcher(String)} instead
* @see AntPathRequestMatcher
*/
@Deprecated
public HttpSecurity antMatcher(String antPattern) {
return requestMatcher(new AntPathRequestMatcher(antPattern));
}
@ -3608,8 +3929,10 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<Defaul
* </p>
* @param mvcPattern the Spring MVC Pattern to match on (i.e. "/admin/**")
* @return the {@link HttpSecurity} for further customizations
* @deprecated use {@link #securityMatcher(String)} instead
* @see MvcRequestMatcher
*/
@Deprecated
public HttpSecurity mvcMatcher(String mvcPattern) {
HandlerMappingIntrospector introspector = new HandlerMappingIntrospector(getContext());
return requestMatcher(new MvcRequestMatcher(introspector, mvcPattern));
@ -3628,8 +3951,10 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<Defaul
* </p>
* @param pattern the Regular Expression to match on (i.e. "/admin/.+")
* @return the {@link HttpSecurity} for further customizations
* @see RegexRequestMatcher
* @deprecated use {@link #securityMatcher(RequestMatcher)} with a
* {@link RegexRequestMatcher} instead
*/
@Deprecated
public HttpSecurity regexMatcher(String pattern) {
return requestMatcher(new RegexRequestMatcher(pattern, null));
}
@ -3700,14 +4025,22 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<Defaul
setApplicationContext(context);
}
/**
* @deprecated use {@link #requestMatchers(HttpMethod, String...)} instead
*/
@Override
@Deprecated
public MvcMatchersRequestMatcherConfigurer mvcMatchers(HttpMethod method, String... mvcPatterns) {
List<MvcRequestMatcher> mvcMatchers = createMvcMatchers(method, mvcPatterns);
setMatchers(mvcMatchers);
return new MvcMatchersRequestMatcherConfigurer(getContext(), mvcMatchers, this.matchers);
}
/**
* @deprecated use {@link #requestMatchers(String...)} instead
*/
@Override
@Deprecated
public MvcMatchersRequestMatcherConfigurer mvcMatchers(String... patterns) {
return mvcMatchers(null, patterns);
}
@ -3720,7 +4053,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<Defaul
private void setMatchers(List<? extends RequestMatcher> requestMatchers) {
this.matchers.addAll(requestMatchers);
requestMatcher(new OrRequestMatcher(this.matchers));
securityMatcher(new OrRequestMatcher(this.matchers));
}
/**

View File

@ -138,7 +138,7 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder<Filter,
* <pre>
* webSecurityBuilder.ignoring()
* // ignore all URLs that start with /resources/ or /static/
* .antMatchers(&quot;/resources/**&quot;, &quot;/static/**&quot;);
* .requestMatchers(&quot;/resources/**&quot;, &quot;/static/**&quot;);
* </pre>
*
* Alternatively this will accomplish the same result:
@ -146,7 +146,7 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder<Filter,
* <pre>
* webSecurityBuilder.ignoring()
* // ignore all URLs that start with /resources/ or /static/
* .antMatchers(&quot;/resources/**&quot;).antMatchers(&quot;/static/**&quot;);
* .requestMatchers(&quot;/resources/**&quot;).requestMatchers(&quot;/static/**&quot;);
* </pre>
*
* Multiple invocations of ignoring() are also additive, so the following is also
@ -155,10 +155,10 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder<Filter,
* <pre>
* webSecurityBuilder.ignoring()
* // ignore all URLs that start with /resources/
* .antMatchers(&quot;/resources/**&quot;);
* .requestMatchers(&quot;/resources/**&quot;);
* webSecurityBuilder.ignoring()
* // ignore all URLs that start with /static/
* .antMatchers(&quot;/static/**&quot;);
* .requestMatchers(&quot;/static/**&quot;);
* // now both URLs that start with /resources/ and /static/ will be ignored
* </pre>
* @return the {@link IgnoredRequestConfigurer} to use for registering request that
@ -401,7 +401,9 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder<Filter,
* {@link MvcRequestMatcher#setMethod(HttpMethod)}
*
* @author Rob Winch
* @deprecated use {@link MvcRequestMatcher.Builder} instead
*/
@Deprecated
public final class MvcMatchersIgnoredRequestConfigurer extends IgnoredRequestConfigurer {
private final List<MvcRequestMatcher> mvcMatchers;
@ -433,14 +435,22 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder<Filter,
setApplicationContext(context);
}
/**
* @deprecated use {@link #requestMatchers(HttpMethod, String...)} instead
*/
@Override
@Deprecated
public MvcMatchersIgnoredRequestConfigurer mvcMatchers(HttpMethod method, String... mvcPatterns) {
List<MvcRequestMatcher> mvcMatchers = createMvcMatchers(method, mvcPatterns);
WebSecurity.this.ignoredRequests.addAll(mvcMatchers);
return new MvcMatchersIgnoredRequestConfigurer(getApplicationContext(), mvcMatchers);
}
/**
* @deprecated use {@link #requestMatchers(String...)} instead
*/
@Override
@Deprecated
public MvcMatchersIgnoredRequestConfigurer mvcMatchers(String... mvcPatterns) {
return mvcMatchers(null, mvcPatterns);
}

View File

@ -42,12 +42,12 @@ import org.springframework.security.web.SecurityFilterChain;
* public WebSecurityCustomizer webSecurityCustomizer() {
* return (web) -> web.ignoring()
* // Spring Security should completely ignore URLs starting with /resources/
* .antMatchers(&quot;/resources/**&quot;);
* .requestMatchers(&quot;/resources/**&quot;);
* }
*
* &#064;Bean
* public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
* http.authorizeRequests().antMatchers(&quot;/public/**&quot;).permitAll().anyRequest()
* http.authorizeRequests().requestMatchers(&quot;/public/**&quot;).permitAll().anyRequest()
* .hasRole(&quot;USER&quot;).and()
* // Possibly more configuration ...
* .formLogin() // enable form based log in

View File

@ -146,12 +146,20 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
return postProcess(this.managerBuilder.build());
}
/**
* @deprecated use {@link #requestMatchers(String...)} instead
*/
@Override
@Deprecated
public MvcMatchersAuthorizedUrl mvcMatchers(String... mvcPatterns) {
return mvcMatchers(null, mvcPatterns);
}
/**
* @deprecated use {@link #requestMatchers(HttpMethod, String...)} instead
*/
@Override
@Deprecated
public MvcMatchersAuthorizedUrl mvcMatchers(HttpMethod method, String... mvcPatterns) {
return new MvcMatchersAuthorizedUrl(createMvcMatchers(method, mvcPatterns));
}
@ -203,7 +211,9 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
* configuring the {@link MvcRequestMatcher#setServletPath(String)}.
*
* @author Evgeniy Cheban
* @deprecated use {@link MvcRequestMatcher.Builder} instead
*/
@Deprecated
public final class MvcMatchersAuthorizedUrl extends AuthorizedUrl {
private MvcMatchersAuthorizedUrl(List<MvcRequestMatcher> matchers) {

View File

@ -163,7 +163,10 @@ public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>>
* </pre>
*
* @since 4.0
* @deprecated use {@link #ignoringRequestMatchers(RequestMatcher...)} with an
* {@link org.springframework.security.web.util.matcher.AntPathRequestMatcher} instead
*/
@Deprecated
public CsrfConfigurer<H> ignoringAntMatchers(String... antPatterns) {
return new IgnoreCsrfProtectionRegistry(this.context).antMatchers(antPatterns).and();
}
@ -197,6 +200,35 @@ public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>>
return new IgnoreCsrfProtectionRegistry(this.context).requestMatchers(requestMatchers).and();
}
/**
* <p>
* Allows specifying {@link HttpServletRequest} that should not use CSRF Protection
* even if they match the {@link #requireCsrfProtectionMatcher(RequestMatcher)}.
* </p>
*
* <p>
* For example, the following configuration will ensure CSRF protection ignores:
* </p>
* <ul>
* <li>Any GET, HEAD, TRACE, OPTIONS (this is the default)</li>
* <li>We also explicitly state to ignore any request that starts with "/sockjs/"</li>
* </ul>
*
* <pre>
* http
* .csrf()
* .ignoringRequestMatchers("/sockjs/**")
* .and()
* ...
* </pre>
*
* @since 5.8
* @see AbstractRequestMatcherRegistry#requestMatchers(String...)
*/
public CsrfConfigurer<H> ignoringRequestMatchers(String... patterns) {
return new IgnoreCsrfProtectionRegistry(this.context).requestMatchers(patterns).and();
}
/**
* <p>
* Specify the {@link SessionAuthenticationStrategy} to use. The default is a
@ -350,14 +382,22 @@ public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>>
setApplicationContext(context);
}
/**
* @deprecated use {@link #requestMatchers(HttpMethod, String...)} instead
*/
@Override
@Deprecated
public MvcMatchersIgnoreCsrfProtectionRegistry mvcMatchers(HttpMethod method, String... mvcPatterns) {
List<MvcRequestMatcher> mvcMatchers = createMvcMatchers(method, mvcPatterns);
CsrfConfigurer.this.ignoredCsrfProtectionMatchers.addAll(mvcMatchers);
return new MvcMatchersIgnoreCsrfProtectionRegistry(getApplicationContext(), mvcMatchers);
}
/**
* @deprecated use {@link #requestMatchers(String...)} instead
*/
@Override
@Deprecated
public MvcMatchersIgnoreCsrfProtectionRegistry mvcMatchers(String... mvcPatterns) {
return mvcMatchers(null, mvcPatterns);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2022 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.
@ -42,6 +42,7 @@ class CsrfDsl {
private var ignoringAntMatchers: Array<out String>? = null
private var ignoringRequestMatchers: Array<out RequestMatcher>? = null
private var ignoringRequestMatchersPatterns: Array<out String>? = null
private var disabled = false
/**
@ -66,6 +67,16 @@ class CsrfDsl {
ignoringRequestMatchers = requestMatchers
}
/**
* Allows specifying [HttpServletRequest]s that should not use CSRF Protection
* even if they match the [requireCsrfProtectionMatcher].
*
* @param patterns the patterns that should not use CSRF protection
*/
fun ignoringRequestMatchers(vararg patterns: String) {
ignoringRequestMatchersPatterns = patterns
}
/**
* Disable CSRF protection
*/
@ -80,6 +91,7 @@ class CsrfDsl {
sessionAuthenticationStrategy?.also { csrf.sessionAuthenticationStrategy(sessionAuthenticationStrategy) }
ignoringAntMatchers?.also { csrf.ignoringAntMatchers(*ignoringAntMatchers!!) }
ignoringRequestMatchers?.also { csrf.ignoringRequestMatchers(*ignoringRequestMatchers!!) }
ignoringRequestMatchersPatterns?.also { csrf.ignoringRequestMatchers(*ignoringRequestMatchersPatterns!!) }
if (disabled) {
csrf.disable()
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2022 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.
@ -16,6 +16,8 @@
package org.springframework.security.config.annotation.web;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.List;
import javax.servlet.DispatcherType;
@ -23,13 +25,20 @@ import javax.servlet.DispatcherType;
import org.junit.jupiter.api.BeforeEach;
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.annotation.ObjectPostProcessor;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
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 static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link AbstractRequestMatcherRegistry}.
@ -38,11 +47,21 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
public class AbstractRequestMatcherRegistryTests {
private static final ObjectPostProcessor<Object> NO_OP_OBJECT_POST_PROCESSOR = new ObjectPostProcessor<Object>() {
@Override
public <O> O postProcess(O object) {
return object;
}
};
private TestRequestMatcherRegistry matcherRegistry;
@BeforeEach
public void setUp() {
this.matcherRegistry = new TestRequestMatcherRegistry();
ApplicationContext context = mock(ApplicationContext.class);
given(context.getBean(ObjectPostProcessor.class)).willReturn(NO_OP_OBJECT_POST_PROCESSOR);
this.matcherRegistry.setApplicationContext(context);
}
@Test
@ -94,6 +113,91 @@ public class AbstractRequestMatcherRegistryTests {
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(DispatcherTypeRequestMatcher.class);
}
@Test
public void requestMatchersWhenPatternAndMvcPresentThenReturnMvcRequestMatcherType() throws Exception {
mockMvcPresentClasspath(true);
mockMvcIntrospector(true);
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers("/path");
assertThat(requestMatchers).isNotEmpty();
assertThat(requestMatchers.size()).isEqualTo(1);
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(MvcRequestMatcher.class);
}
@Test
public void requestMatchersWhenHttpMethodAndPatternAndMvcPresentThenReturnMvcRequestMatcherType() throws Exception {
mockMvcPresentClasspath(true);
mockMvcIntrospector(true);
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers(HttpMethod.GET, "/path");
assertThat(requestMatchers).isNotEmpty();
assertThat(requestMatchers.size()).isEqualTo(1);
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(MvcRequestMatcher.class);
}
@Test
public void requestMatchersWhenHttpMethodAndMvcPresentThenReturnMvcRequestMatcherType() throws Exception {
mockMvcPresentClasspath(true);
mockMvcIntrospector(true);
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers(HttpMethod.GET);
assertThat(requestMatchers).isNotEmpty();
assertThat(requestMatchers.size()).isEqualTo(1);
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(MvcRequestMatcher.class);
}
@Test
public void requestMatchersWhenPatternAndMvcNotPresentThenReturnAntPathRequestMatcherType() throws Exception {
mockMvcPresentClasspath(false);
mockMvcIntrospector(false);
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers("/path");
assertThat(requestMatchers).isNotEmpty();
assertThat(requestMatchers.size()).isEqualTo(1);
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class);
}
@Test
public void requestMatchersWhenHttpMethodAndPatternAndMvcNotPresentThenReturnAntPathRequestMatcherType()
throws Exception {
mockMvcPresentClasspath(false);
mockMvcIntrospector(false);
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers(HttpMethod.GET, "/path");
assertThat(requestMatchers).isNotEmpty();
assertThat(requestMatchers.size()).isEqualTo(1);
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class);
}
@Test
public void requestMatchersWhenHttpMethodAndMvcNotPresentThenReturnAntPathMatcherType() throws Exception {
mockMvcPresentClasspath(false);
mockMvcIntrospector(false);
List<RequestMatcher> requestMatchers = this.matcherRegistry.requestMatchers(HttpMethod.GET);
assertThat(requestMatchers).isNotEmpty();
assertThat(requestMatchers.size()).isEqualTo(1);
assertThat(requestMatchers.get(0)).isExactlyInstanceOf(AntPathRequestMatcher.class);
}
@Test
public void requestMatchersWhenMvcPresentInClassPathAndMvcIntrospectorBeanNotAvailableThenException()
throws Exception {
mockMvcPresentClasspath(true);
mockMvcIntrospector(false);
assertThatExceptionOfType(NoSuchBeanDefinitionException.class)
.isThrownBy(() -> this.matcherRegistry.requestMatchers("/path")).withMessageContaining(
"Please ensure Spring Security & Spring MVC are configured in a shared ApplicationContext");
}
private void mockMvcIntrospector(boolean isPresent) {
ApplicationContext context = this.matcherRegistry.getApplicationContext();
given(context.containsBean("mvcHandlerMappingIntrospector")).willReturn(isPresent);
}
private void mockMvcPresentClasspath(Object newValue) throws Exception {
Field mvcPresentField = AbstractRequestMatcherRegistry.class.getDeclaredField("mvcPresent");
mvcPresentField.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(mvcPresentField, mvcPresentField.getModifiers() & ~Modifier.FINAL);
mvcPresentField.set(null, newValue);
}
private static class TestRequestMatcherRegistry extends AbstractRequestMatcherRegistry<List<RequestMatcher>> {
@Override

View File

@ -20,17 +20,21 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
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.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@ -79,6 +83,22 @@ public class CsrfConfigurerIgnoringRequestMatchersTests {
this.mvc.perform(put("/no-csrf")).andExpect(status().isOk());
}
@Test
public void requestWhenIgnoringRequestMatcherPatternThenIgnores() throws Exception {
this.spring.register(IgnoringPathsAndMatchersPatternConfig.class, BasicController.class).autowire();
this.mvc.perform(put("/csrf")).andExpect(status().isForbidden());
this.mvc.perform(post("/csrf")).andExpect(status().isForbidden());
this.mvc.perform(put("/no-csrf")).andExpect(status().isOk());
}
@Test
public void requestWhenIgnoringRequestMatcherPatternInLambdaThenIgnores() throws Exception {
this.spring.register(IgnoringPathsAndMatchersPatternInLambdaConfig.class, BasicController.class).autowire();
this.mvc.perform(put("/csrf")).andExpect(status().isForbidden());
this.mvc.perform(post("/csrf")).andExpect(status().isForbidden());
this.mvc.perform(put("/no-csrf")).andExpect(status().isOk());
}
@EnableWebSecurity
static class IgnoringRequestMatchers extends WebSecurityConfigurerAdapter {
@ -151,6 +171,41 @@ public class CsrfConfigurerIgnoringRequestMatchersTests {
}
@Configuration
@EnableWebSecurity
@EnableWebMvc
static class IgnoringPathsAndMatchersPatternConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.csrf()
.ignoringRequestMatchers("/no-csrf");
// @formatter:on
return http.build();
}
}
@Configuration
@EnableWebSecurity
@EnableWebMvc
static class IgnoringPathsAndMatchersPatternInLambdaConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.csrf((csrf) -> csrf
.ignoringRequestMatchers("/no-csrf")
);
// @formatter:on
return http.build();
}
}
@RestController
public static class BasicController {

View File

@ -0,0 +1,567 @@
/*
* Copyright 2002-2022 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.lang.reflect.Field;
import java.lang.reflect.Modifier;
import javax.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockServletContext;
import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.security.config.Customizer.withDefaults;
/**
* @author Rob Winch
*
*/
public class HttpSecuritySecurityMatchersTests {
AnnotationConfigWebApplicationContext context;
MockHttpServletRequest request;
MockHttpServletResponse response;
MockFilterChain chain;
@Autowired
FilterChainProxy springSecurityFilterChain;
@BeforeEach
public void setup() throws Exception {
this.request = new MockHttpServletRequest("GET", "");
this.request.setMethod("GET");
this.response = new MockHttpServletResponse();
this.chain = new MockFilterChain();
mockMvcPresentClasspath(true);
}
@AfterEach
public void cleanup() {
if (this.context != null) {
this.context.close();
}
}
@Test
public void securityMatcherWhenMvcThenMvcMatcher() throws Exception {
loadConfig(SecurityMatcherMvcConfig.class, LegacyMvcMatchingConfig.class);
this.request.setServletPath("/path");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
setup();
this.request.setServletPath("/path.html");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
setup();
this.request.setServletPath("/path/");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
}
@Test
public void securityMatcherWhenNoMvcThenAntMatcher() throws Exception {
mockMvcPresentClasspath(false);
loadConfig(SecurityMatcherNoMvcConfig.class, LegacyMvcMatchingConfig.class);
this.request.setServletPath("/path");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
setup();
this.request.setServletPath("/path.html");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
setup();
this.request.setServletPath("/path/");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
}
@Test
public void securityMatcherWhenMvcMatcherAndGetFiltersNoUnsupportedMethodExceptionFromDummyRequest() {
loadConfig(SecurityMatcherMvcConfig.class);
assertThat(this.springSecurityFilterChain.getFilters("/path")).isNotEmpty();
}
@Test
public void securityMatchersWhenMvcThenMvcMatcher() throws Exception {
loadConfig(SecurityMatchersMvcMatcherConfig.class, LegacyMvcMatchingConfig.class);
this.request.setServletPath("/path");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
setup();
this.request.setServletPath("/path.html");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
setup();
this.request.setServletPath("/path/");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
}
@Test
public void securityMatchersWhenMvcMatcherInLambdaThenPathIsSecured() throws Exception {
loadConfig(SecurityMatchersMvcMatcherInLambdaConfig.class, LegacyMvcMatchingConfig.class);
this.request.setServletPath("/path");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
setup();
this.request.setServletPath("/path.html");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
setup();
this.request.setServletPath("/path/");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
}
@Test
public void securityMatchersMvcMatcherServletPath() throws Exception {
loadConfig(SecurityMatchersMvcMatcherServletPathConfig.class);
this.request.setServletPath("/spring");
this.request.setRequestURI("/spring/path");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
setup();
this.request.setServletPath("");
this.request.setRequestURI("/path");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
setup();
this.request.setServletPath("/other");
this.request.setRequestURI("/other/path");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
}
@Test
public void securityMatchersWhensMvcMatcherServletPathInLambdaThenPathIsSecured() throws Exception {
loadConfig(SecurityMatchersMvcMatcherServletPathInLambdaConfig.class);
this.request.setServletPath("/spring");
this.request.setRequestURI("/spring/path");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
setup();
this.request.setServletPath("");
this.request.setRequestURI("/path");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
setup();
this.request.setServletPath("/other");
this.request.setRequestURI("/other/path");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
}
@Test
public void securityMatchersWhenMultiMvcMatcherInLambdaThenAllPathsAreDenied() throws Exception {
loadConfig(MultiMvcMatcherInLambdaConfig.class);
this.request.setRequestURI("/test-1");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
setup();
this.request.setRequestURI("/test-2");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
setup();
this.request.setRequestURI("/test-3");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
}
@Test
public void securityMatchersWhenMultiMvcMatcherThenAllPathsAreDenied() throws Exception {
loadConfig(MultiMvcMatcherConfig.class);
this.request.setRequestURI("/test-1");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
setup();
this.request.setRequestURI("/test-2");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
setup();
this.request.setRequestURI("/test-3");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
}
public void loadConfig(Class<?>... configs) {
this.context = new AnnotationConfigWebApplicationContext();
this.context.register(configs);
this.context.setServletContext(new MockServletContext());
this.context.refresh();
this.context.getAutowireCapableBeanFactory().autowireBean(this);
}
private void mockMvcPresentClasspath(Object newValue) throws Exception {
mockMvcPresentClasspath(HttpSecurity.class, newValue);
mockMvcPresentClasspath(AbstractRequestMatcherRegistry.class, newValue);
}
private void mockMvcPresentClasspath(Class<?> clazz, Object newValue) throws Exception {
Field mvcPresentField = clazz.getDeclaredField("mvcPresent");
mvcPresentField.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(mvcPresentField, mvcPresentField.getModifiers() & ~Modifier.FINAL);
mvcPresentField.set(null, newValue);
}
@EnableWebSecurity
@Configuration
@EnableWebMvc
static class MultiMvcMatcherInLambdaConfig {
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
SecurityFilterChain first(HttpSecurity http) throws Exception {
// @formatter:off
http
.securityMatchers((requests) -> requests
.requestMatchers("/test-1")
.requestMatchers("/test-2")
.requestMatchers("/test-3")
)
.authorizeHttpRequests((authorize) -> authorize.anyRequest().denyAll())
.httpBasic(withDefaults());
// @formatter:on
return http.build();
}
@Bean
SecurityFilterChain second(HttpSecurity http) throws Exception {
// @formatter:off
http
.securityMatchers((requests) -> requests
.requestMatchers("/test-1")
)
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().permitAll()
);
// @formatter:on
return http.build();
}
@RestController
static class PathController {
@RequestMapping({ "/test-1", "/test-2", "/test-3" })
String path() {
return "path";
}
}
}
@EnableWebSecurity
@Configuration
@EnableWebMvc
static class MultiMvcMatcherConfig {
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
SecurityFilterChain first(HttpSecurity http) throws Exception {
// @formatter:off
http
.securityMatchers()
.requestMatchers("/test-1")
.requestMatchers("/test-2")
.requestMatchers("/test-3")
.and()
.authorizeHttpRequests()
.anyRequest().denyAll()
.and()
.httpBasic(withDefaults());
// @formatter:on
return http.build();
}
@Bean
SecurityFilterChain second(HttpSecurity http) throws Exception {
// @formatter:off
http
.securityMatchers()
.requestMatchers("/test-1")
.and()
.authorizeHttpRequests()
.anyRequest().permitAll();
// @formatter:on
return http.build();
}
@RestController
static class PathController {
@RequestMapping({ "/test-1", "/test-2", "/test-3" })
String path() {
return "path";
}
}
}
@EnableWebSecurity
@EnableWebMvc
@Configuration
@Import(UsersConfig.class)
static class SecurityMatcherMvcConfig {
@Bean
SecurityFilterChain appSecurity(HttpSecurity http) throws Exception {
// @formatter:off
http
.securityMatcher("/path")
.httpBasic().and()
.authorizeHttpRequests()
.anyRequest().denyAll();
// @formatter:on
return http.build();
}
@RestController
static class PathController {
@RequestMapping("/path")
String path() {
return "path";
}
}
}
@EnableWebSecurity
@Configuration
@Import(UsersConfig.class)
static class SecurityMatcherNoMvcConfig {
@Bean
SecurityFilterChain appSecurity(HttpSecurity http) throws Exception {
// @formatter:off
http
.securityMatcher("/path")
.httpBasic().and()
.authorizeHttpRequests()
.anyRequest().denyAll();
// @formatter:on
return http.build();
}
@RestController
static class PathController {
@RequestMapping("/path")
String path() {
return "path";
}
}
}
@EnableWebSecurity
@Configuration
@EnableWebMvc
@Import(UsersConfig.class)
static class SecurityMatchersMvcMatcherConfig {
@Bean
SecurityFilterChain appSecurity(HttpSecurity http) throws Exception {
// @formatter:off
http
.securityMatchers()
.requestMatchers("/path")
.and()
.httpBasic().and()
.authorizeHttpRequests()
.anyRequest().denyAll();
// @formatter:on
return http.build();
}
@RestController
static class PathController {
@RequestMapping("/path")
String path() {
return "path";
}
}
}
@EnableWebSecurity
@Configuration
@EnableWebMvc
static class SecurityMatchersMvcMatcherInLambdaConfig {
@Bean
SecurityFilterChain appSecurity(HttpSecurity http) throws Exception {
// @formatter:off
http
.securityMatchers((matchers) -> matchers
.requestMatchers("/path")
)
.httpBasic(withDefaults())
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().denyAll()
);
// @formatter:on
return http.build();
}
@RestController
static class PathController {
@RequestMapping("/path")
String path() {
return "path";
}
}
}
@EnableWebSecurity
@Configuration
@EnableWebMvc
@Import(UsersConfig.class)
static class SecurityMatchersMvcMatcherServletPathConfig {
@Bean
SecurityFilterChain appSecurity(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception {
MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector)
.servletPath("/spring");
// @formatter:off
http
.securityMatchers()
.requestMatchers(mvcMatcherBuilder.pattern("/path"))
.requestMatchers(mvcMatcherBuilder.pattern("/never-match"))
.and()
.httpBasic().and()
.authorizeHttpRequests()
.anyRequest().denyAll();
// @formatter:on
return http.build();
}
@RestController
static class PathController {
@RequestMapping("/path")
String path() {
return "path";
}
}
}
@EnableWebSecurity
@Configuration
@EnableWebMvc
@Import(UsersConfig.class)
static class SecurityMatchersMvcMatcherServletPathInLambdaConfig {
@Bean
SecurityFilterChain appSecurity(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception {
MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector)
.servletPath("/spring");
// @formatter:off
http
.securityMatchers((matchers) -> matchers
.requestMatchers(mvcMatcherBuilder.pattern("/path"))
.requestMatchers(mvcMatcherBuilder.pattern("/never-match"))
)
.httpBasic(withDefaults())
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().denyAll()
);
// @formatter:on
return http.build();
}
@RestController
static class PathController {
@RequestMapping("/path")
String path() {
return "path";
}
}
}
@Configuration
static class UsersConfig {
@Bean
UserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder().username("user").password("password").roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}
@Configuration
static class LegacyMvcMatchingConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setUseSuffixPatternMatch(true);
configurer.setUseTrailingSlashMatch(true);
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 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.
@ -23,6 +23,7 @@ import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
@ -33,6 +34,7 @@ import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.provisioning.InMemoryUserDetailsManager
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy
import org.springframework.security.web.csrf.CsrfTokenRepository
import org.springframework.security.web.csrf.DefaultCsrfToken
@ -43,6 +45,7 @@ import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.post
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.servlet.config.annotation.EnableWebMvc
/**
* Tests for [CsrfDsl]
@ -257,6 +260,38 @@ class CsrfDslTests {
}
}
@Test
fun `CSRF when ignoring request matchers pattern then CSRF disabled on matching requests`() {
this.spring.register(IgnoringRequestMatchersPatternConfig::class.java, BasicController::class.java).autowire()
this.mockMvc.post("/test1")
.andExpect {
status { isForbidden() }
}
this.mockMvc.post("/test2")
.andExpect {
status { isOk() }
}
}
@Configuration
@EnableWebSecurity
@EnableWebMvc
open class IgnoringRequestMatchersPatternConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
csrf {
requireCsrfProtectionMatcher = AntPathRequestMatcher("/**")
ignoringRequestMatchers("/test2")
}
}
return http.build()
}
}
@RestController
internal class BasicController {
@PostMapping("/test1")

View File

@ -109,13 +109,14 @@ SecurityFilterChain web(HttpSecurity http, AuthorizationManager<RequestAuthoriza
@Bean
AuthorizationManager<RequestAuthorizationContext> requestMatcherAuthorizationManager(HandlerMappingIntrospector introspector) {
MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector);
RequestMatcher permitAll =
new AndRequestMatcher(
new MvcRequestMatcher(introspector, "/resources/**"),
new MvcRequestMatcher(introspector, "/signup"),
new MvcRequestMatcher(introspector, "/about"));
RequestMatcher admin = new MvcRequestMatcher(introspector, "/admin/**");
RequestMatcher db = new MvcRequestMatcher(introspector, "/db/**");
mvcMatcherBuilder.pattern("/resources/**"),
mvcMatcherBuilder.pattern("/signup"),
mvcMatcherBuilder.pattern("/about"));
RequestMatcher admin = mvcMatcherBuilder.pattern("/admin/**");
RequestMatcher db = mvcMatcherBuilder.pattern("/db/**");
RequestMatcher any = AnyRequestMatcher.INSTANCE;
AuthorizationManager<HttpRequestServlet> manager = RequestMatcherDelegatingAuthorizationManager.builder()
.add(permitAll, (context) -> new AuthorizationDecision(true))

View File

@ -145,7 +145,7 @@ You could refer to the method using:
----
http
.authorizeHttpRequests(authorize -> authorize
.antMatchers("/user/**").access("@webSecurity.check(authentication,request)")
.requestMatchers("/user/**").access("@webSecurity.check(authentication,request)")
...
)
----
@ -211,7 +211,7 @@ You could refer to the method using:
----
http
.authorizeHttpRequests(authorize -> authorize
.antMatchers("/user/{userId}/**").access("@webSecurity.checkUserId(authentication,#userId)")
.requestMatchers("/user/{userId}/**").access("@webSecurity.checkUserId(authentication,#userId)")
...
);
----

View File

@ -139,8 +139,8 @@ If we wanted to restrict access to this controller method to admin users, a deve
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.antMatchers("/admin").hasRole("ADMIN")
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/admin").hasRole("ADMIN")
);
return http.build();
}
@ -152,8 +152,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeRequests {
authorize(AntPathRequestMatcher("/admin"), hasRole("ADMIN"))
authorizeHttpRequests {
authorize("/admin", hasRole("ADMIN"))
}
}
return http.build()
@ -177,21 +177,24 @@ Additionally, depending on our Spring MVC configuration, the URL `/admin/` will
The problem is that our security rule is only protecting `/admin`.
We could add additional rules for all the permutations of Spring MVC, but this would be quite verbose and tedious.
Instead, we can leverage Spring Security's `MvcRequestMatcher`.
The following configuration will protect the same URLs that Spring MVC will match on by using Spring MVC to match on the URL.
Fortunately, when using the `requestMatchers` DSL method, Spring Security automatically creates a `MvcRequestMatcher` if it detects that Spring MVC is available in the classpath.
Therefore, it will protect the same URLs that Spring MVC will match on by using Spring MVC to match on the URL.
One common requirement when using Spring MVC is to specify the servlet path property, for that you can use the `MvcRequestMatcher.Builder` to create multiple `MvcRequestMatcher` instances that share the same servlet path:
====
.Java
[source,java,role="primary"]
----
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
public SecurityFilterChain filterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception {
MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector).servletPath("/path");
http
.authorizeHttpRequests(authorize -> authorize
.mvcMatchers("/admin").hasRole("ADMIN")
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(mvcMatcherBuilder.pattern("/admin")).hasRole("ADMIN")
.requestMatchers(mvcMatcherBuilder.pattern("/user")).hasRole("USER")
);
// ...
return http.build();
}
----
@ -199,25 +202,19 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
[source,kotlin,role="secondary"]
----
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
open fun filterChain(http: HttpSecurity, introspector: HandlerMappingIntrospector): SecurityFilterChain {
val mvcMatcherBuilder = MvcRequestMatcher.Builder(introspector)
http {
authorizeRequests {
authorize("/admin", hasRole("ADMIN"))
authorizeHttpRequests {
authorize(mvcMatcherBuilder.pattern("/admin"), hasRole("ADMIN"))
authorize(mvcMatcherBuilder.pattern("/user"), hasRole("USER"))
}
}
// ...
return http.build()
}
----
====
or in XML
[source,xml]
----
<http request-matcher="mvc">
<intercept-url pattern="/admin" access="hasRole('ADMIN')"/>
</http>
----
[[mvc-authentication-principal]]
== @AuthenticationPrincipal

View File

@ -567,7 +567,7 @@ public class WebSecurityConfig {
http
.csrf(csrf -> csrf
// ignore our stomp endpoints since they are protected using Stomp headers
.ignoringAntMatchers("/chat/**")
.ignoringRequestMatchers("/chat/**")
)
.headers(headers -> headers
// allow same origin to frame our site to support iframe SockJS
@ -591,7 +591,7 @@ open class WebSecurityConfig {
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
csrf {
ignoringAntMatchers("/chat/**")
ignoringRequestMatchers("/chat/**")
}
headers {
frameOptions {

View File

@ -11,3 +11,5 @@ Below are the highlights of the release.
* https://github.com/spring-projects/spring-security/pull/11232[gh-11232] - `ClientRegistrations#rest` defines 30s connect and read timeouts
* https://github.com/spring-projects/spring-security/pull/11464[gh-11464] - Remember Me supports SHA256 algorithm
* https://github.com/spring-projects/spring-security/pull/11908[gh-11908] - Make X-Xss-Protection header value configurable in ServerHttpSecurity
* https://github.com/spring-projects/spring-security/issues/11347[gh-11347] - Simplify Java Configuration `RequestMatcher` Usage
* https://github.com/spring-projects/spring-security/issues/9159[gh-9159] - Add `securityMatcher` as an alias on `requestMatcher` in `HttpSecurity`

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2022 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.
@ -172,4 +172,59 @@ public class MvcRequestMatcher implements RequestMatcher, RequestVariablesExtrac
}
/**
* A builder for {@link MvcRequestMatcher}
*
* @author Marcus Da Coregio
* @since 5.8
*/
public static final class Builder {
private final HandlerMappingIntrospector introspector;
private String servletPath;
/**
* Construct a new instance of this builder
*/
public Builder(HandlerMappingIntrospector introspector) {
this.introspector = introspector;
}
/**
* Sets the servlet path to be used by the {@link MvcRequestMatcher} generated by
* this builder
* @param servletPath the servlet path to use
* @return the {@link Builder} for further configuration
*/
public Builder servletPath(String servletPath) {
this.servletPath = servletPath;
return this;
}
/**
* Creates an {@link MvcRequestMatcher} that uses the provided pattern to match
* @param pattern the pattern used to match
* @return the generated {@link MvcRequestMatcher}
*/
public MvcRequestMatcher pattern(String pattern) {
return pattern(null, pattern);
}
/**
* Creates an {@link MvcRequestMatcher} that uses the provided pattern and HTTP
* method to match
* @param method the {@link HttpMethod}, can be null
* @param pattern the patterns used to match
* @return the generated {@link MvcRequestMatcher}
*/
public MvcRequestMatcher pattern(HttpMethod method, String pattern) {
MvcRequestMatcher mvcRequestMatcher = new MvcRequestMatcher(this.introspector, pattern);
mvcRequestMatcher.setServletPath(this.servletPath);
mvcRequestMatcher.setMethod(method);
return mvcRequestMatcher;
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2022 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.
@ -28,6 +28,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpMethod;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
import org.springframework.web.servlet.handler.MatchableHandlerMapping;
@ -245,4 +246,30 @@ public class MvcRequestMatcherTests {
assertThat(this.matcher.matcher(this.request).isMatch()).isTrue();
}
@Test
public void builderWhenServletPathThenServletPathPresent() {
MvcRequestMatcher matcher = new MvcRequestMatcher.Builder(this.introspector).servletPath("/path")
.pattern("/endpoint");
assertThat(matcher.getServletPath()).isEqualTo("/path");
assertThat(ReflectionTestUtils.getField(matcher, "pattern")).isEqualTo("/endpoint");
assertThat(ReflectionTestUtils.getField(matcher, "method")).isNull();
}
@Test
public void builderWhenPatternThenPatternPresent() {
MvcRequestMatcher matcher = new MvcRequestMatcher.Builder(this.introspector).pattern("/endpoint");
assertThat(matcher.getServletPath()).isNull();
assertThat(ReflectionTestUtils.getField(matcher, "pattern")).isEqualTo("/endpoint");
assertThat(ReflectionTestUtils.getField(matcher, "method")).isNull();
}
@Test
public void builderWhenMethodAndPatternThenMethodAndPatternPresent() {
MvcRequestMatcher matcher = new MvcRequestMatcher.Builder(this.introspector).pattern(HttpMethod.GET,
"/endpoint");
assertThat(matcher.getServletPath()).isNull();
assertThat(ReflectionTestUtils.getField(matcher, "pattern")).isEqualTo("/endpoint");
assertThat(ReflectionTestUtils.getField(matcher, "method")).isEqualTo(HttpMethod.GET);
}
}