From 039e0328e1af034412231c6ece2f5e92a313cc84 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Wed, 21 Sep 2022 10:09:35 -0300 Subject: [PATCH] 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 --- .../web/AbstractRequestMatcherRegistry.java | 103 +++- .../annotation/web/builders/HttpSecurity.java | 339 ++++++++++- .../annotation/web/builders/WebSecurity.java | 18 +- .../web/configuration/EnableWebSecurity.java | 4 +- .../AuthorizeHttpRequestsConfigurer.java | 10 + .../web/configurers/CsrfConfigurer.java | 40 ++ .../security/config/web/servlet/CsrfDsl.kt | 14 +- .../AbstractRequestMatcherRegistryTests.java | 106 +++- ...onfigurerIgnoringRequestMatchersTests.java | 55 ++ .../HttpSecuritySecurityMatchersTests.java | 567 ++++++++++++++++++ .../config/web/servlet/CsrfDslTests.kt | 37 +- .../authorize-http-requests.adoc | 11 +- .../authorization/expression-based.adoc | 4 +- .../ROOT/pages/servlet/integrations/mvc.adoc | 41 +- .../pages/servlet/integrations/websocket.adoc | 4 +- docs/modules/ROOT/pages/whats-new.adoc | 2 + .../util/matcher/MvcRequestMatcher.java | 57 +- .../util/matcher/MvcRequestMatcherTests.java | 29 +- 18 files changed, 1395 insertions(+), 46 deletions(-) create mode 100644 config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecuritySecurityMatchersTests.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java b/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java index 473b9ef4ad..9760353c7b 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java @@ -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 { 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 { * 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 { * @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 { * @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 { * @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 { * @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 { * @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 { * @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 { return chainRequestMatchers(Arrays.asList(requestMatchers)); } + /** + *

+ * 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}. + *

+ *

+ * If a specific {@link RequestMatcher} must be specified, use + * {@link #requestMatchers(RequestMatcher...)} instead + *

+ * @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 matchers = new ArrayList<>(); + if (mvcPresent) { + matchers.addAll(createMvcMatchers(method, patterns)); + } + else { + matchers.addAll(RequestMatchers.antMatchers(method, patterns)); + } + return requestMatchers(matchers.toArray(new RequestMatcher[0])); + } + + /** + *

+ * 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}. + *

+ *

+ * If a specific {@link RequestMatcher} must be specified, use + * {@link #requestMatchers(RequestMatcher...)} instead + *

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

+ * 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}. + *

+ *

+ * If a specific {@link RequestMatcher} must be specified, use + * {@link #requestMatchers(RequestMatcher...)} instead + *

+ * @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. diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java index 3a230d6940..e687ef1e1e 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java @@ -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; * * @Bean * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - * http.authorizeRequests().antMatchers("/**").hasRole("USER").and().formLogin(); + * http.authorizeHttpRequests().requestMatchers("/**").hasRole("USER").and().formLogin(); * return http.build(); * } * @@ -141,6 +143,12 @@ import org.springframework.web.servlet.handler.HandlerMappingIntrospector; public final class HttpSecurity extends AbstractConfiguredSecurityBuilder implements SecurityBuilder, HttpSecurityBuilder { + 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 filters = new ArrayList<>(); @@ -151,6 +159,10 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder * @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 requestMatcherCustomizer) { requestMatcherCustomizer.customize(this.requestMatcherConfigurer); return HttpSecurity.this; @@ -3567,15 +3583,318 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder + * Invoking {@link #securityMatchers()} will not override previous invocations of + * {@link #securityMatchers()}}, {@link #securityMatchers(Customizer)} + * {@link #securityMatcher(String)} and {@link #securityMatcher(RequestMatcher)} + *

+ * + *

Example Configurations

+ * + * The following configuration enables the {@link HttpSecurity} for URLs that begin + * with "/api/" or "/oauth/". + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class RequestMatchersSecurityConfig {
+	 *
+	 * 	@Bean
+	 * 	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.securityMatchers((matchers) -> matchers
+	 * 				.requestMatchers("/api/**", "/oauth/**")
+	 * 			)
+	 * 			.authorizeHttpRequests((authorize) -> authorize
+	 * 				anyRequest().hasRole("USER")
+	 * 			)
+	 * 			.httpBasic(withDefaults());
+	 * 		return http.build();
+	 * 	}
+	 *
+	 * 	@Bean
+	 * 	public UserDetailsService userDetailsService() {
+	 * 		UserDetails user = User.withDefaultPasswordEncoder()
+	 * 			.username("user")
+	 * 			.password("password")
+	 * 			.roles("USER")
+	 * 			.build();
+	 * 		return new InMemoryUserDetailsManager(user);
+	 * 	}
+	 * }
+	 * 
+ * + * The configuration below is the same as the previous configuration. + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class RequestMatchersSecurityConfig {
+	 *
+	 * 	@Bean
+	 * 	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.securityMatchers((matchers) -> matchers
+	 * 				.requestMatchers("/api/**")
+	 * 				.requestMatchers("/oauth/**")
+	 * 			)
+	 * 			.authorizeHttpRequests((authorize) -> authorize
+	 * 				anyRequest().hasRole("USER")
+	 * 			)
+	 * 			.httpBasic(withDefaults());
+	 * 		return http.build();
+	 * 	}
+	 *
+	 * 	@Bean
+	 * 	public UserDetailsService userDetailsService() {
+	 * 		UserDetails user = User.withDefaultPasswordEncoder()
+	 * 			.username("user")
+	 * 			.password("password")
+	 * 			.roles("USER")
+	 * 			.build();
+	 * 		return new InMemoryUserDetailsManager(user);
+	 * 	}
+	 * }
+	 * 
+ * + * The configuration below is also the same as the above configuration. + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class RequestMatchersSecurityConfig {
+	 *
+	 * 	@Bean
+	 * 	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.securityMatchers((matchers) -> matchers
+	 * 				.requestMatchers("/api/**")
+	 * 			)
+	 *			.securityMatchers((matchers) -> matchers
+	 *				.requestMatchers("/oauth/**")
+	 * 			)
+	 * 			.authorizeHttpRequests((authorize) -> authorize
+	 * 				anyRequest().hasRole("USER")
+	 * 			)
+	 * 			.httpBasic(withDefaults());
+	 * 		return http.build();
+	 * 	}
+	 *
+	 * 	@Bean
+	 * 	public UserDetailsService userDetailsService() {
+	 * 		UserDetails user = User.withDefaultPasswordEncoder()
+	 * 			.username("user")
+	 * 			.password("password")
+	 * 			.roles("USER")
+	 * 			.build();
+	 * 		return new InMemoryUserDetailsManager(user);
+	 * 	}
+	 * }
+	 * 
+ * @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)}. + * + *

+ * Invoking {@link #securityMatchers(Customizer)} will not override previous + * invocations of {@link #securityMatchers()}}, {@link #securityMatchers(Customizer)} + * {@link #securityMatcher(String)} and {@link #securityMatcher(RequestMatcher)} + *

+ * + *

Example Configurations

+ * + * The following configuration enables the {@link HttpSecurity} for URLs that begin + * with "/api/" or "/oauth/". + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class RequestMatchersSecurityConfig {
+	 *
+	 * 	@Bean
+	 * 	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.securityMatchers((matchers) -> matchers
+	 * 				.requestMatchers("/api/**", "/oauth/**")
+	 * 			)
+	 * 			.authorizeHttpRequests((authorize) -> authorize
+	 * 				.anyRequest().hasRole("USER")
+	 * 			)
+	 * 			.httpBasic(withDefaults());
+	 * 		return http.build();
+	 * 	}
+	 *
+	 * 	@Bean
+	 * 	public UserDetailsService userDetailsService() {
+	 * 		UserDetails user = User.withDefaultPasswordEncoder()
+	 * 			.username("user")
+	 * 			.password("password")
+	 * 			.roles("USER")
+	 * 			.build();
+	 * 		return new InMemoryUserDetailsManager(user);
+	 * 	}
+	 * }
+	 * 
+ * + * The configuration below is the same as the previous configuration. + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class RequestMatchersSecurityConfig {
+	 *
+	 * 	@Bean
+	 * 	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.securityMatchers((matchers) -> matchers
+	 * 				.requestMatchers("/api/**")
+	 * 				.requestMatchers("/oauth/**")
+	 * 			)
+	 * 			.authorizeHttpRequests((authorize) -> authorize
+	 * 				.anyRequest().hasRole("USER")
+	 * 			)
+	 * 			.httpBasic(withDefaults());
+	 * 		return http.build();
+	 * 	}
+	 *
+	 * 	@Bean
+	 * 	public UserDetailsService userDetailsService() {
+	 * 		UserDetails user = User.withDefaultPasswordEncoder()
+	 * 			.username("user")
+	 * 			.password("password")
+	 * 			.roles("USER")
+	 * 			.build();
+	 * 		return new InMemoryUserDetailsManager(user);
+	 * 	}
+	 * }
+	 * 
+ * + * The configuration below is also the same as the above configuration. + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class RequestMatchersSecurityConfig {
+	 *
+	 * 	@Bean
+	 * 	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.securityMatchers((matchers) -> matchers
+	 * 				.requestMatchers("/api/**")
+	 * 			)
+	 *			.securityMatchers((matchers) -> matchers
+	 *				.requestMatchers("/oauth/**")
+	 * 			)
+	 * 			.authorizeHttpRequests((authorize) -> authorize
+	 * 				.anyRequest().hasRole("USER")
+	 * 			)
+	 * 			.httpBasic(withDefaults());
+	 * 		return http.build();
+	 * 	}
+	 *
+	 * 	@Bean
+	 * 	public UserDetailsService userDetailsService() {
+	 * 		UserDetails user = User.withDefaultPasswordEncoder()
+	 * 			.username("user")
+	 * 			.password("password")
+	 * 			.roles("USER")
+	 * 			.build();
+	 * 		return new InMemoryUserDetailsManager(user);
+	 * 	}
+	 * }
+	 * 
+ * @param requestMatcherCustomizer the {@link Customizer} to provide more options for + * the {@link RequestMatcherConfigurer} + * @return the {@link HttpSecurity} for further customizations + */ + public HttpSecurity securityMatchers(Customizer 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)} ()}. + * + *

+ * 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)} + *

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

+ * Invoking {@link #securityMatcher(String)} will override previous invocations of + * {@link #mvcMatcher(String)}}, {@link #requestMatchers()}, + * {@link #antMatcher(String)}, {@link #regexMatcher(String)}, and + * {@link #requestMatcher(RequestMatcher)}. + *

+ * @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 * @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 * @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 * @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 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 requestMatchers) { this.matchers.addAll(requestMatchers); - requestMatcher(new OrRequestMatcher(this.matchers)); + securityMatcher(new OrRequestMatcher(this.matchers)); } /** diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java index 1548efdf6b..9a26c546a6 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java @@ -138,7 +138,7 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder * webSecurityBuilder.ignoring() * // ignore all URLs that start with /resources/ or /static/ - * .antMatchers("/resources/**", "/static/**"); + * .requestMatchers("/resources/**", "/static/**"); * * * Alternatively this will accomplish the same result: @@ -146,7 +146,7 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder * webSecurityBuilder.ignoring() * // ignore all URLs that start with /resources/ or /static/ - * .antMatchers("/resources/**").antMatchers("/static/**"); + * .requestMatchers("/resources/**").requestMatchers("/static/**"); * * * Multiple invocations of ignoring() are also additive, so the following is also @@ -155,10 +155,10 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder * webSecurityBuilder.ignoring() * // ignore all URLs that start with /resources/ - * .antMatchers("/resources/**"); + * .requestMatchers("/resources/**"); * webSecurityBuilder.ignoring() * // ignore all URLs that start with /static/ - * .antMatchers("/static/**"); + * .requestMatchers("/static/**"); * // now both URLs that start with /resources/ and /static/ will be ignored * * @return the {@link IgnoredRequestConfigurer} to use for registering request that @@ -401,7 +401,9 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder mvcMatchers; @@ -433,14 +435,22 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder 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); } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java index b81d91421b..de9c022f52 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java @@ -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("/resources/**"); + * .requestMatchers("/resources/**"); * } * * @Bean * public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - * http.authorizeRequests().antMatchers("/public/**").permitAll().anyRequest() + * http.authorizeRequests().requestMatchers("/public/**").permitAll().anyRequest() * .hasRole("USER").and() * // Possibly more configuration ... * .formLogin() // enable form based log in diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java index 0dba2308d0..e19922896b 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java @@ -146,12 +146,20 @@ public final class AuthorizeHttpRequestsConfigurer matchers) { diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java index 24c4c24693..3f0ff7a477 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java @@ -163,7 +163,10 @@ public final class CsrfConfigurer> * * * @since 4.0 + * @deprecated use {@link #ignoringRequestMatchers(RequestMatcher...)} with an + * {@link org.springframework.security.web.util.matcher.AntPathRequestMatcher} instead */ + @Deprecated public CsrfConfigurer ignoringAntMatchers(String... antPatterns) { return new IgnoreCsrfProtectionRegistry(this.context).antMatchers(antPatterns).and(); } @@ -197,6 +200,35 @@ public final class CsrfConfigurer> return new IgnoreCsrfProtectionRegistry(this.context).requestMatchers(requestMatchers).and(); } + /** + *

+ * Allows specifying {@link HttpServletRequest} that should not use CSRF Protection + * even if they match the {@link #requireCsrfProtectionMatcher(RequestMatcher)}. + *

+ * + *

+ * For example, the following configuration will ensure CSRF protection ignores: + *

+ *
    + *
  • Any GET, HEAD, TRACE, OPTIONS (this is the default)
  • + *
  • We also explicitly state to ignore any request that starts with "/sockjs/"
  • + *
+ * + *
+	 * http
+	 *     .csrf()
+	 *         .ignoringRequestMatchers("/sockjs/**")
+	 *         .and()
+	 *     ...
+	 * 
+ * + * @since 5.8 + * @see AbstractRequestMatcherRegistry#requestMatchers(String...) + */ + public CsrfConfigurer ignoringRequestMatchers(String... patterns) { + return new IgnoreCsrfProtectionRegistry(this.context).requestMatchers(patterns).and(); + } + /** *

* Specify the {@link SessionAuthenticationStrategy} to use. The default is a @@ -350,14 +382,22 @@ public final class CsrfConfigurer> setApplicationContext(context); } + /** + * @deprecated use {@link #requestMatchers(HttpMethod, String...)} instead + */ @Override + @Deprecated public MvcMatchersIgnoreCsrfProtectionRegistry mvcMatchers(HttpMethod method, String... mvcPatterns) { List 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); } diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/CsrfDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/CsrfDsl.kt index f0120e369d..55faf074cc 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/servlet/CsrfDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/CsrfDsl.kt @@ -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? = null private var ignoringRequestMatchers: Array? = null + private var ignoringRequestMatchersPatterns: Array? = 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() } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryTests.java index 1b38cac950..e222c403db 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryTests.java @@ -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 NO_OP_OBJECT_POST_PROCESSOR = new ObjectPostProcessor() { + @Override + public 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 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 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 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 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 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 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> { @Override diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerIgnoringRequestMatchersTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerIgnoringRequestMatchersTests.java index 9bc906b013..e630c05306 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerIgnoringRequestMatchersTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerIgnoringRequestMatchersTests.java @@ -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 { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecuritySecurityMatchersTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecuritySecurityMatchersTests.java new file mode 100644 index 0000000000..3b9f6287a1 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecuritySecurityMatchersTests.java @@ -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); + } + + } + +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/CsrfDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/CsrfDslTests.kt index 19b885e29b..1888bdfd47 100644 --- a/config/src/test/kotlin/org/springframework/security/config/web/servlet/CsrfDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/CsrfDslTests.kt @@ -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") diff --git a/docs/modules/ROOT/pages/servlet/authorization/authorize-http-requests.adoc b/docs/modules/ROOT/pages/servlet/authorization/authorize-http-requests.adoc index e3005ed304..2d14eb9771 100644 --- a/docs/modules/ROOT/pages/servlet/authorization/authorize-http-requests.adoc +++ b/docs/modules/ROOT/pages/servlet/authorization/authorize-http-requests.adoc @@ -109,13 +109,14 @@ SecurityFilterChain web(HttpSecurity http, AuthorizationManager 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 manager = RequestMatcherDelegatingAuthorizationManager.builder() .add(permitAll, (context) -> new AuthorizationDecision(true)) diff --git a/docs/modules/ROOT/pages/servlet/authorization/expression-based.adoc b/docs/modules/ROOT/pages/servlet/authorization/expression-based.adoc index 218b6dec37..24a7ef0980 100644 --- a/docs/modules/ROOT/pages/servlet/authorization/expression-based.adoc +++ b/docs/modules/ROOT/pages/servlet/authorization/expression-based.adoc @@ -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)") ... ); ---- diff --git a/docs/modules/ROOT/pages/servlet/integrations/mvc.adoc b/docs/modules/ROOT/pages/servlet/integrations/mvc.adoc index 212c545ccc..0af922acbb 100644 --- a/docs/modules/ROOT/pages/servlet/integrations/mvc.adoc +++ b/docs/modules/ROOT/pages/servlet/integrations/mvc.adoc @@ -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] ----- - - - ----- [[mvc-authentication-principal]] == @AuthenticationPrincipal diff --git a/docs/modules/ROOT/pages/servlet/integrations/websocket.adoc b/docs/modules/ROOT/pages/servlet/integrations/websocket.adoc index 348bf54737..27a535e1b0 100644 --- a/docs/modules/ROOT/pages/servlet/integrations/websocket.adoc +++ b/docs/modules/ROOT/pages/servlet/integrations/websocket.adoc @@ -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 { diff --git a/docs/modules/ROOT/pages/whats-new.adoc b/docs/modules/ROOT/pages/whats-new.adoc index 42f255ed9c..0514f6af69 100644 --- a/docs/modules/ROOT/pages/whats-new.adoc +++ b/docs/modules/ROOT/pages/whats-new.adoc @@ -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` diff --git a/web/src/main/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcher.java b/web/src/main/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcher.java index 70c96faba5..7aca310392 100644 --- a/web/src/main/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcher.java @@ -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; + } + + } + } diff --git a/web/src/test/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcherTests.java b/web/src/test/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcherTests.java index 759cabd1a8..8c9504ffb9 100644 --- a/web/src/test/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcherTests.java +++ b/web/src/test/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcherTests.java @@ -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); + } + }