Skip to content
Merged
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 @@ -281,3 +281,134 @@ open fun web(http: HttpSecurity): SecurityFilterChain {
}
----
====

== Request Matchers

The `RequestMatcher` interface is used to determine if a request matches a given rule.
We use `securityMatchers` to determine if a given `HttpSecurity` should be applied to a given request.
The same way, we can use `requestMatchers` to determine the authorization rules that we should apply to a given request.
Look at the following example:

====
.Java
[source,java,role="primary"]
----
@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**") <1>
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/user/**").hasRole("USER") <2>
.requestMatchers("/admin/**").hasRole("ADMIN") <3>
.anyRequest().authenticated() <4>
)
.formLogin(withDefaults());
return http.build();
}
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Configuration
@EnableWebSecurity
open class SecurityConfig {

@Bean
open fun web(http: HttpSecurity): SecurityFilterChain {
http {
securityMatcher("/api/**") <1>
authorizeHttpRequests {
authorize("/user/**", hasRole("USER")) <2>
authorize("/admin/**", hasRole("ADMIN")) <3>
authorize(anyRequest, authenticated) <4>
}
}
return http.build()
}

}
----
====

<1> Configure `HttpSecurity` to only be applied to URLs that start with `/api/`
<2> Allow access to URLs that start with `/user/` to users with the `USER` role
<3> Allow access to URLs that start with `/admin/` to users with the `ADMIN` role
<4> Any other request that doesn't match the rules above, will require authentication

The `securityMatcher(s)` and `requestMatcher(s)` methods will decide which `RequestMatcher` implementation fits best for your application: If Spring MVC is in the classpath, then `MvcRequestMatcher` will be used, otherwise, `AntPathRequestMatcher` will be used.
You can read more about the Spring MVC integration xref:servlet/integrations/mvc.adoc[here].

If you want to use a specific `RequestMatcher`, just pass an implementation to the `securityMatcher` and/or `requestMatcher` methods:

====
.Java
[source,java,role="primary"]
----
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; <1>
import static org.springframework.security.web.util.matcher.RegexRequestMatcher.regexMatcher;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher(antMatcher("/api/**")) <2>
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(antMatcher("/user/**")).hasRole("USER") <3>
.requestMatchers(regexMatcher("/admin/.*")).hasRole("ADMIN") <4>
.requestMatchers(new MyCustomRequestMatcher()).hasRole("SUPERVISOR") <5>
.anyRequest().authenticated()
)
.formLogin(withDefaults());
return http.build();
}
}

public class MyCustomRequestMatcher implements RequestMatcher {

@Override
public boolean matches(HttpServletRequest request) {
// ...
}
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
import org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher <1>
import org.springframework.security.web.util.matcher.RegexRequestMatcher.regexMatcher

@Configuration
@EnableWebSecurity
open class SecurityConfig {

@Bean
open fun web(http: HttpSecurity): SecurityFilterChain {
http {
securityMatcher(antMatcher("/api/**")) <2>
authorizeHttpRequests {
authorize(antMatcher("/user/**"), hasRole("USER")) <3>
authorize(regexMatcher("/admin/**"), hasRole("ADMIN")) <4>
authorize(MyCustomRequestMatcher(), hasRole("SUPERVISOR")) <5>
authorize(anyRequest, authenticated)
}
}
return http.build()
}

}
----
====

<1> Import the static factory methods from `AntPathRequestMatcher` and `RegexRequestMatcher` to create `RequestMatcher` instances.
<2> Configure `HttpSecurity` to only be applied to URLs that start with `/api/`, using `AntPathRequestMatcher`
<3> Allow access to URLs that start with `/user/` to users with the `USER` role, using `AntPathRequestMatcher`
<4> Allow access to URLs that start with `/admin/` to users with the `ADMIN` role, using `RegexRequestMatcher`
<5> Allow access to URLs that match the `MyCustomRequestMatcher` to users with the `SUPERVISOR` role, using a custom `RequestMatcher`
2 changes: 2 additions & 0 deletions etc/checkstyle/checkstyle.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
<property name="avoidStaticImportExcludes" value="org.springframework.security.test.web.servlet.response.SecurityMockMvcResultHandlers.*" />
<property name="avoidStaticImportExcludes" value="org.springframework.security.config.annotation.SecurityContextChangedListenerArgumentMatchers.*" />
<property name="avoidStaticImportExcludes" value="org.springframework.security.web.csrf.CsrfTokenAssert.*" />
<property name="avoidStaticImportExcludes" value="org.springframework.security.web.util.matcher.AntPathRequestMatcher.*" />
<property name="avoidStaticImportExcludes" value="org.springframework.security.web.util.matcher.RegexRequestMatcher.*" />
</module>
<module name="com.puppycrawl.tools.checkstyle.TreeWalker">
<module name="com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineJavaCheck">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,43 @@ public final class AntPathRequestMatcher implements RequestMatcher, RequestVaria

private final UrlPathHelper urlPathHelper;

/**
* Creates a matcher with the specific pattern which will match all HTTP methods in a
* case-sensitive manner.
* @param pattern the ant pattern to use for matching
* @since 5.8
*/
public static AntPathRequestMatcher antMatcher(String pattern) {
Assert.hasText(pattern, "pattern cannot be empty");
return new AntPathRequestMatcher(pattern);
}

/**
* Creates a matcher that will match all request with the supplied HTTP method in a
* case-sensitive manner.
* @param method the HTTP method. The {@code matches} method will return false if the
* incoming request doesn't have the same method.
* @since 5.8
*/
public static AntPathRequestMatcher antMatcher(HttpMethod method) {
Assert.notNull(method, "method cannot be null");
return new AntPathRequestMatcher(MATCH_ALL, method.name());
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There doesn't seem to be a way to specify a pattern or a method and pattern via static factory method

/**
* Creates a matcher with the supplied pattern and HTTP method in a case-sensitive
* manner.
* @param method the HTTP method. The {@code matches} method will return false if the
* incoming request doesn't have the same method.
* @param pattern the ant pattern to use for matching
* @since 5.8
*/
public static AntPathRequestMatcher antMatcher(HttpMethod method, String pattern) {
Assert.notNull(method, "method cannot be null");
Assert.hasText(pattern, "pattern cannot be empty");
return new AntPathRequestMatcher(pattern, method.name());
}

/**
* Creates a matcher with the specific pattern which will match all HTTP methods in a
* case sensitive manner.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

import org.springframework.core.log.LogMessage;
import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
Expand Down Expand Up @@ -53,6 +54,38 @@ public final class RegexRequestMatcher implements RequestMatcher {

private final HttpMethod httpMethod;

/**
* Creates a case-sensitive {@code Pattern} instance to match against the request.
* @param pattern the regular expression to compile into a pattern.
* @since 5.8
*/
public static RegexRequestMatcher regexMatcher(String pattern) {
Assert.hasText(pattern, "pattern cannot be empty");
return new RegexRequestMatcher(pattern, null);
}

/**
* Creates an instance that matches to all requests with the same {@link HttpMethod}.
* @param method the HTTP method to match. Must not be null.
* @since 5.8
*/
public static RegexRequestMatcher regexMatcher(HttpMethod method) {
Assert.notNull(method, "method cannot be null");
return new RegexRequestMatcher(".*", method.name());
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There doesn't seem to be a static factory method for both the method and pattern.

/**
* Creates a case-sensitive {@code Pattern} instance to match against the request.
* @param method the HTTP method to match. May be null to match all methods.
* @param pattern the regular expression to compile into a pattern.
* @since 5.8
*/
public static RegexRequestMatcher regexMatcher(HttpMethod method, String pattern) {
Assert.notNull(method, "method cannot be null");
Assert.hasText(pattern, "pattern cannot be empty");
return new RegexRequestMatcher(pattern, method.name());
}

/**
* Creates a case-sensitive {@code Pattern} instance to match against the request.
* @param pattern the regular expression to compile into a pattern.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,15 @@
import org.mockito.Mock;
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.util.UrlPathHelper;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.BDDMockito.given;
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher;

/**
* @author Luke Taylor
Expand Down Expand Up @@ -205,6 +209,48 @@ public void matcherWhenMatchAllPatternThenMatchResult() {
assertThat(matcher.matcher(request).isMatch()).isTrue();
}

@Test
public void staticAntMatcherWhenPatternProvidedThenPattern() {
AntPathRequestMatcher matcher = antMatcher("/path");
assertThat(matcher.getPattern()).isEqualTo("/path");
}

@Test
public void staticAntMatcherWhenMethodProvidedThenMatchAll() {
AntPathRequestMatcher matcher = antMatcher(HttpMethod.GET);
assertThat(ReflectionTestUtils.getField(matcher, "httpMethod")).isEqualTo(HttpMethod.GET);
}

@Test
public void staticAntMatcherWhenMethodAndPatternProvidedThenMatchAll() {
AntPathRequestMatcher matcher = antMatcher(HttpMethod.POST, "/path");
assertThat(matcher.getPattern()).isEqualTo("/path");
assertThat(ReflectionTestUtils.getField(matcher, "httpMethod")).isEqualTo(HttpMethod.POST);
}

@Test
public void staticAntMatcherWhenMethodNullThenException() {
assertThatIllegalArgumentException().isThrownBy(() -> antMatcher((HttpMethod) null))
.withMessage("method cannot be null");
}

@Test
public void staticAntMatcherWhenPatternNullThenException() {
assertThatIllegalArgumentException().isThrownBy(() -> antMatcher((String) null))
.withMessage("pattern cannot be empty");
}

@Test
public void forMethodWhenMethodThenMatches() {
AntPathRequestMatcher matcher = antMatcher(HttpMethod.POST);
MockHttpServletRequest request = createRequest("/path");
assertThat(matcher.matches(request)).isTrue();
request.setServletPath("/another-path/second");
assertThat(matcher.matches(request)).isTrue();
request.setMethod("GET");
assertThat(matcher.matches(request)).isFalse();
}

private HttpServletRequest createRequestWithNullMethod(String path) {
given(this.request.getServletPath()).willReturn(path);
return this.request;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import org.springframework.http.HttpMethod;
import org.springframework.mock.web.MockHttpServletRequest;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.BDDMockito.given;
import static org.springframework.security.web.util.matcher.RegexRequestMatcher.regexMatcher;

/**
* @author Luke Taylor
Expand Down Expand Up @@ -123,6 +126,46 @@ public void toStringThenFormatted() {
assertThat(matcher.toString()).isEqualTo("Regex [pattern='/blah', GET]");
}

@Test
public void matchesWhenRequestUriMatchesThenMatchesTrue() {
RegexRequestMatcher matcher = regexMatcher(".*");
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/something/anything");
assertThat(matcher.matches(request)).isTrue();
}

@Test
public void matchesWhenRequestUriDontMatchThenMatchesFalse() {
RegexRequestMatcher matcher = regexMatcher(".*\\?param=value");
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/something/anything");
assertThat(matcher.matches(request)).isFalse();
}

@Test
public void matchesWhenRequestMethodMatchesThenMatchesTrue() {
RegexRequestMatcher matcher = regexMatcher(HttpMethod.GET);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/something/anything");
assertThat(matcher.matches(request)).isTrue();
}

@Test
public void matchesWhenRequestMethodDontMatchThenMatchesFalse() {
RegexRequestMatcher matcher = regexMatcher(HttpMethod.POST);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/something/anything");
assertThat(matcher.matches(request)).isFalse();
}

@Test
public void staticRegexMatcherWhenNoPatternThenException() {
assertThatIllegalArgumentException().isThrownBy(() -> regexMatcher((String) null))
.withMessage("pattern cannot be empty");
}

@Test
public void staticRegexMatcherNoMethodThenException() {
assertThatIllegalArgumentException().isThrownBy(() -> regexMatcher((HttpMethod) null))
.withMessage("method cannot be null");
}

private HttpServletRequest createRequestWithNullMethod(String path) {
given(this.request.getQueryString()).willReturn("doesntMatter");
given(this.request.getServletPath()).willReturn(path);
Expand Down