Skip to content

Commit c5e35bf

Browse files
Merge branch '5.8.x'
Closes gh-11978
2 parents 27059ce + 4b6fed0 commit c5e35bf

File tree

6 files changed

+292
-0
lines changed

6 files changed

+292
-0
lines changed

Diff for: docs/modules/ROOT/pages/servlet/authorization/authorize-http-requests.adoc

+131
Original file line numberDiff line numberDiff line change
@@ -277,3 +277,134 @@ open fun web(http: HttpSecurity): SecurityFilterChain {
277277
}
278278
----
279279
====
280+
281+
== Request Matchers
282+
283+
The `RequestMatcher` interface is used to determine if a request matches a given rule.
284+
We use `securityMatchers` to determine if a given `HttpSecurity` should be applied to a given request.
285+
The same way, we can use `requestMatchers` to determine the authorization rules that we should apply to a given request.
286+
Look at the following example:
287+
288+
====
289+
.Java
290+
[source,java,role="primary"]
291+
----
292+
@Configuration
293+
@EnableWebSecurity
294+
public class SecurityConfig {
295+
296+
@Bean
297+
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
298+
http
299+
.securityMatcher("/api/**") <1>
300+
.authorizeHttpRequests(authorize -> authorize
301+
.requestMatchers("/user/**").hasRole("USER") <2>
302+
.requestMatchers("/admin/**").hasRole("ADMIN") <3>
303+
.anyRequest().authenticated() <4>
304+
)
305+
.formLogin(withDefaults());
306+
return http.build();
307+
}
308+
}
309+
----
310+
.Kotlin
311+
[source,kotlin,role="secondary"]
312+
----
313+
@Configuration
314+
@EnableWebSecurity
315+
open class SecurityConfig {
316+
317+
@Bean
318+
open fun web(http: HttpSecurity): SecurityFilterChain {
319+
http {
320+
securityMatcher("/api/**") <1>
321+
authorizeHttpRequests {
322+
authorize("/user/**", hasRole("USER")) <2>
323+
authorize("/admin/**", hasRole("ADMIN")) <3>
324+
authorize(anyRequest, authenticated) <4>
325+
}
326+
}
327+
return http.build()
328+
}
329+
330+
}
331+
----
332+
====
333+
334+
<1> Configure `HttpSecurity` to only be applied to URLs that start with `/api/`
335+
<2> Allow access to URLs that start with `/user/` to users with the `USER` role
336+
<3> Allow access to URLs that start with `/admin/` to users with the `ADMIN` role
337+
<4> Any other request that doesn't match the rules above, will require authentication
338+
339+
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.
340+
You can read more about the Spring MVC integration xref:servlet/integrations/mvc.adoc[here].
341+
342+
If you want to use a specific `RequestMatcher`, just pass an implementation to the `securityMatcher` and/or `requestMatcher` methods:
343+
344+
====
345+
.Java
346+
[source,java,role="primary"]
347+
----
348+
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; <1>
349+
import static org.springframework.security.web.util.matcher.RegexRequestMatcher.regexMatcher;
350+
351+
@Configuration
352+
@EnableWebSecurity
353+
public class SecurityConfig {
354+
355+
@Bean
356+
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
357+
http
358+
.securityMatcher(antMatcher("/api/**")) <2>
359+
.authorizeHttpRequests(authorize -> authorize
360+
.requestMatchers(antMatcher("/user/**")).hasRole("USER") <3>
361+
.requestMatchers(regexMatcher("/admin/.*")).hasRole("ADMIN") <4>
362+
.requestMatchers(new MyCustomRequestMatcher()).hasRole("SUPERVISOR") <5>
363+
.anyRequest().authenticated()
364+
)
365+
.formLogin(withDefaults());
366+
return http.build();
367+
}
368+
}
369+
370+
public class MyCustomRequestMatcher implements RequestMatcher {
371+
372+
@Override
373+
public boolean matches(HttpServletRequest request) {
374+
// ...
375+
}
376+
}
377+
----
378+
.Kotlin
379+
[source,kotlin,role="secondary"]
380+
----
381+
import org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher <1>
382+
import org.springframework.security.web.util.matcher.RegexRequestMatcher.regexMatcher
383+
384+
@Configuration
385+
@EnableWebSecurity
386+
open class SecurityConfig {
387+
388+
@Bean
389+
open fun web(http: HttpSecurity): SecurityFilterChain {
390+
http {
391+
securityMatcher(antMatcher("/api/**")) <2>
392+
authorizeHttpRequests {
393+
authorize(antMatcher("/user/**"), hasRole("USER")) <3>
394+
authorize(regexMatcher("/admin/**"), hasRole("ADMIN")) <4>
395+
authorize(MyCustomRequestMatcher(), hasRole("SUPERVISOR")) <5>
396+
authorize(anyRequest, authenticated)
397+
}
398+
}
399+
return http.build()
400+
}
401+
402+
}
403+
----
404+
====
405+
406+
<1> Import the static factory methods from `AntPathRequestMatcher` and `RegexRequestMatcher` to create `RequestMatcher` instances.
407+
<2> Configure `HttpSecurity` to only be applied to URLs that start with `/api/`, using `AntPathRequestMatcher`
408+
<3> Allow access to URLs that start with `/user/` to users with the `USER` role, using `AntPathRequestMatcher`
409+
<4> Allow access to URLs that start with `/admin/` to users with the `ADMIN` role, using `RegexRequestMatcher`
410+
<5> Allow access to URLs that match the `MyCustomRequestMatcher` to users with the `SUPERVISOR` role, using a custom `RequestMatcher`

Diff for: etc/checkstyle/checkstyle.xml

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
<property name="avoidStaticImportExcludes" value="org.springframework.security.test.web.servlet.response.SecurityMockMvcResultHandlers.*" />
1919
<property name="avoidStaticImportExcludes" value="org.springframework.security.config.annotation.SecurityContextChangedListenerArgumentMatchers.*" />
2020
<property name="avoidStaticImportExcludes" value="org.springframework.security.web.csrf.CsrfTokenAssert.*" />
21+
<property name="avoidStaticImportExcludes" value="org.springframework.security.web.util.matcher.AntPathRequestMatcher.*" />
22+
<property name="avoidStaticImportExcludes" value="org.springframework.security.web.util.matcher.RegexRequestMatcher.*" />
2123
</module>
2224
<module name="com.puppycrawl.tools.checkstyle.TreeWalker">
2325
<module name="com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineJavaCheck">

Diff for: web/src/main/java/org/springframework/security/web/util/matcher/AntPathRequestMatcher.java

+37
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,43 @@ public final class AntPathRequestMatcher implements RequestMatcher, RequestVaria
6767

6868
private final UrlPathHelper urlPathHelper;
6969

70+
/**
71+
* Creates a matcher with the specific pattern which will match all HTTP methods in a
72+
* case-sensitive manner.
73+
* @param pattern the ant pattern to use for matching
74+
* @since 5.8
75+
*/
76+
public static AntPathRequestMatcher antMatcher(String pattern) {
77+
Assert.hasText(pattern, "pattern cannot be empty");
78+
return new AntPathRequestMatcher(pattern);
79+
}
80+
81+
/**
82+
* Creates a matcher that will match all request with the supplied HTTP method in a
83+
* case-sensitive manner.
84+
* @param method the HTTP method. The {@code matches} method will return false if the
85+
* incoming request doesn't have the same method.
86+
* @since 5.8
87+
*/
88+
public static AntPathRequestMatcher antMatcher(HttpMethod method) {
89+
Assert.notNull(method, "method cannot be null");
90+
return new AntPathRequestMatcher(MATCH_ALL, method.name());
91+
}
92+
93+
/**
94+
* Creates a matcher with the supplied pattern and HTTP method in a case-sensitive
95+
* manner.
96+
* @param method the HTTP method. The {@code matches} method will return false if the
97+
* incoming request doesn't have the same method.
98+
* @param pattern the ant pattern to use for matching
99+
* @since 5.8
100+
*/
101+
public static AntPathRequestMatcher antMatcher(HttpMethod method, String pattern) {
102+
Assert.notNull(method, "method cannot be null");
103+
Assert.hasText(pattern, "pattern cannot be empty");
104+
return new AntPathRequestMatcher(pattern, method.name());
105+
}
106+
70107
/**
71108
* Creates a matcher with the specific pattern which will match all HTTP methods in a
72109
* case sensitive manner.

Diff for: web/src/main/java/org/springframework/security/web/util/matcher/RegexRequestMatcher.java

+33
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
import org.springframework.core.log.LogMessage;
2626
import org.springframework.http.HttpMethod;
27+
import org.springframework.util.Assert;
2728
import org.springframework.util.StringUtils;
2829

2930
/**
@@ -52,6 +53,38 @@ public final class RegexRequestMatcher implements RequestMatcher {
5253

5354
private final HttpMethod httpMethod;
5455

56+
/**
57+
* Creates a case-sensitive {@code Pattern} instance to match against the request.
58+
* @param pattern the regular expression to compile into a pattern.
59+
* @since 5.8
60+
*/
61+
public static RegexRequestMatcher regexMatcher(String pattern) {
62+
Assert.hasText(pattern, "pattern cannot be empty");
63+
return new RegexRequestMatcher(pattern, null);
64+
}
65+
66+
/**
67+
* Creates an instance that matches to all requests with the same {@link HttpMethod}.
68+
* @param method the HTTP method to match. Must not be null.
69+
* @since 5.8
70+
*/
71+
public static RegexRequestMatcher regexMatcher(HttpMethod method) {
72+
Assert.notNull(method, "method cannot be null");
73+
return new RegexRequestMatcher(".*", method.name());
74+
}
75+
76+
/**
77+
* Creates a case-sensitive {@code Pattern} instance to match against the request.
78+
* @param method the HTTP method to match. May be null to match all methods.
79+
* @param pattern the regular expression to compile into a pattern.
80+
* @since 5.8
81+
*/
82+
public static RegexRequestMatcher regexMatcher(HttpMethod method, String pattern) {
83+
Assert.notNull(method, "method cannot be null");
84+
Assert.hasText(pattern, "pattern cannot be empty");
85+
return new RegexRequestMatcher(pattern, method.name());
86+
}
87+
5588
/**
5689
* Creates a case-sensitive {@code Pattern} instance to match against the request.
5790
* @param pattern the regular expression to compile into a pattern.

Diff for: web/src/test/java/org/springframework/security/web/util/matcher/AntPathRequestMatcherTests.java

+46
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,15 @@
2222
import org.mockito.Mock;
2323
import org.mockito.junit.jupiter.MockitoExtension;
2424

25+
import org.springframework.http.HttpMethod;
2526
import org.springframework.mock.web.MockHttpServletRequest;
27+
import org.springframework.test.util.ReflectionTestUtils;
2628
import org.springframework.web.util.UrlPathHelper;
2729

2830
import static org.assertj.core.api.Assertions.assertThat;
31+
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
2932
import static org.mockito.BDDMockito.given;
33+
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher;
3034

3135
/**
3236
* @author Luke Taylor
@@ -204,6 +208,48 @@ public void matcherWhenMatchAllPatternThenMatchResult() {
204208
assertThat(matcher.matcher(request).isMatch()).isTrue();
205209
}
206210

211+
@Test
212+
public void staticAntMatcherWhenPatternProvidedThenPattern() {
213+
AntPathRequestMatcher matcher = antMatcher("/path");
214+
assertThat(matcher.getPattern()).isEqualTo("/path");
215+
}
216+
217+
@Test
218+
public void staticAntMatcherWhenMethodProvidedThenMatchAll() {
219+
AntPathRequestMatcher matcher = antMatcher(HttpMethod.GET);
220+
assertThat(ReflectionTestUtils.getField(matcher, "httpMethod")).isEqualTo(HttpMethod.GET);
221+
}
222+
223+
@Test
224+
public void staticAntMatcherWhenMethodAndPatternProvidedThenMatchAll() {
225+
AntPathRequestMatcher matcher = antMatcher(HttpMethod.POST, "/path");
226+
assertThat(matcher.getPattern()).isEqualTo("/path");
227+
assertThat(ReflectionTestUtils.getField(matcher, "httpMethod")).isEqualTo(HttpMethod.POST);
228+
}
229+
230+
@Test
231+
public void staticAntMatcherWhenMethodNullThenException() {
232+
assertThatIllegalArgumentException().isThrownBy(() -> antMatcher((HttpMethod) null))
233+
.withMessage("method cannot be null");
234+
}
235+
236+
@Test
237+
public void staticAntMatcherWhenPatternNullThenException() {
238+
assertThatIllegalArgumentException().isThrownBy(() -> antMatcher((String) null))
239+
.withMessage("pattern cannot be empty");
240+
}
241+
242+
@Test
243+
public void forMethodWhenMethodThenMatches() {
244+
AntPathRequestMatcher matcher = antMatcher(HttpMethod.POST);
245+
MockHttpServletRequest request = createRequest("/path");
246+
assertThat(matcher.matches(request)).isTrue();
247+
request.setServletPath("/another-path/second");
248+
assertThat(matcher.matches(request)).isTrue();
249+
request.setMethod("GET");
250+
assertThat(matcher.matches(request)).isFalse();
251+
}
252+
207253
private HttpServletRequest createRequestWithNullMethod(String path) {
208254
given(this.request.getServletPath()).willReturn(path);
209255
return this.request;

Diff for: web/src/test/java/org/springframework/security/web/util/matcher/RegexRequestMatcherTests.java

+43
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,13 @@
2222
import org.mockito.Mock;
2323
import org.mockito.junit.jupiter.MockitoExtension;
2424

25+
import org.springframework.http.HttpMethod;
2526
import org.springframework.mock.web.MockHttpServletRequest;
2627

2728
import static org.assertj.core.api.Assertions.assertThat;
29+
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
2830
import static org.mockito.BDDMockito.given;
31+
import static org.springframework.security.web.util.matcher.RegexRequestMatcher.regexMatcher;
2932

3033
/**
3134
* @author Luke Taylor
@@ -122,6 +125,46 @@ public void toStringThenFormatted() {
122125
assertThat(matcher.toString()).isEqualTo("Regex [pattern='/blah', GET]");
123126
}
124127

128+
@Test
129+
public void matchesWhenRequestUriMatchesThenMatchesTrue() {
130+
RegexRequestMatcher matcher = regexMatcher(".*");
131+
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/something/anything");
132+
assertThat(matcher.matches(request)).isTrue();
133+
}
134+
135+
@Test
136+
public void matchesWhenRequestUriDontMatchThenMatchesFalse() {
137+
RegexRequestMatcher matcher = regexMatcher(".*\\?param=value");
138+
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/something/anything");
139+
assertThat(matcher.matches(request)).isFalse();
140+
}
141+
142+
@Test
143+
public void matchesWhenRequestMethodMatchesThenMatchesTrue() {
144+
RegexRequestMatcher matcher = regexMatcher(HttpMethod.GET);
145+
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/something/anything");
146+
assertThat(matcher.matches(request)).isTrue();
147+
}
148+
149+
@Test
150+
public void matchesWhenRequestMethodDontMatchThenMatchesFalse() {
151+
RegexRequestMatcher matcher = regexMatcher(HttpMethod.POST);
152+
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/something/anything");
153+
assertThat(matcher.matches(request)).isFalse();
154+
}
155+
156+
@Test
157+
public void staticRegexMatcherWhenNoPatternThenException() {
158+
assertThatIllegalArgumentException().isThrownBy(() -> regexMatcher((String) null))
159+
.withMessage("pattern cannot be empty");
160+
}
161+
162+
@Test
163+
public void staticRegexMatcherNoMethodThenException() {
164+
assertThatIllegalArgumentException().isThrownBy(() -> regexMatcher((HttpMethod) null))
165+
.withMessage("method cannot be null");
166+
}
167+
125168
private HttpServletRequest createRequestWithNullMethod(String path) {
126169
given(this.request.getQueryString()).willReturn("doesntMatter");
127170
given(this.request.getServletPath()).willReturn(path);

0 commit comments

Comments
 (0)