Bläddra i källkod

Add static factory method to AntPathRequestMather and RegexRequestMatcher

Closes gh-11938
Marcus Da Coregio 2 år sedan
förälder
incheckning
4b6fed0667

+ 131 - 0
docs/modules/ROOT/pages/servlet/authorization/authorize-http-requests.adoc

@@ -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 - 0
etc/checkstyle/checkstyle.xml

@@ -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">

+ 37 - 0
web/src/main/java/org/springframework/security/web/util/matcher/AntPathRequestMatcher.java

@@ -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());
+	}
+
+	/**
+	 * 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.

+ 33 - 0
web/src/main/java/org/springframework/security/web/util/matcher/RegexRequestMatcher.java

@@ -25,6 +25,7 @@ import org.apache.commons.logging.LogFactory;
 
 import org.springframework.core.log.LogMessage;
 import org.springframework.http.HttpMethod;
+import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
 
 /**
@@ -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());
+	}
+
+	/**
+	 * 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.

+ 46 - 0
web/src/test/java/org/springframework/security/web/util/matcher/AntPathRequestMatcherTests.java

@@ -23,11 +23,15 @@ import org.junit.jupiter.api.extension.ExtendWith;
 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
@@ -205,6 +209,48 @@ public class AntPathRequestMatcherTests {
 		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;

+ 43 - 0
web/src/test/java/org/springframework/security/web/util/matcher/RegexRequestMatcherTests.java

@@ -23,10 +23,13 @@ import org.junit.jupiter.api.extension.ExtendWith;
 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
@@ -123,6 +126,46 @@ public class RegexRequestMatcherTests {
 		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);