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 78cbb2a4f35..f0395b840ea 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 @@ -41,6 +41,7 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.web.DefaultSecurityFilterChain; import org.springframework.security.web.FilterChainProxy; @@ -69,8 +70,8 @@ * *

* Customizations to the {@link WebSecurity} can be made by creating a - * {@link WebSecurityConfigurer} or more likely by overriding - * {@link WebSecurityConfigurerAdapter}. + * {@link WebSecurityConfigurer}, overriding {@link WebSecurityConfigurerAdapter} or + * exposing a {@link WebSecurityCustomizer} bean. *

* * @author Rob Winch diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java index 27fc4ba5e16..e49c99e0021 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java @@ -77,6 +77,8 @@ public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAwa private List securityFilterChains = Collections.emptyList(); + private List webSecurityCustomizers = Collections.emptyList(); + private ClassLoader beanClassLoader; @Autowired(required = false) @@ -119,6 +121,9 @@ public Filter springSecurityFilterChain() throws Exception { } } } + for (WebSecurityCustomizer customizer : this.webSecurityCustomizers) { + customizer.customize(this.webSecurity); + } return this.webSecurity.build(); } @@ -175,6 +180,12 @@ void setFilterChains(List securityFilterChains) { this.securityFilterChains = securityFilterChains; } + @Autowired(required = false) + void setWebSecurityCustomizers(List webSecurityCustomizers) { + webSecurityCustomizers.sort(AnnotationAwareOrderComparator.INSTANCE); + this.webSecurityCustomizers = webSecurityCustomizers; + } + @Bean public static BeanFactoryPostProcessor conversionServicePostProcessor() { return new RsaKeyConversionServicePostProcessor(); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityCustomizer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityCustomizer.java new file mode 100644 index 00000000000..0a742dd58f5 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityCustomizer.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2020 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.configuration; + +import org.springframework.security.config.annotation.web.builders.WebSecurity; + +/** + * Callback interface for customizing {@link WebSecurity}. + * + * Beans of this type will automatically be used by {@link WebSecurityConfiguration} to + * customize {@link WebSecurity}. + * + * Example usage: + * + *
+ * @Bean
+ * public WebSecurityCustomizer ignoringCustomizer() {
+ * 	return (web) -> web.ignoring().antMatchers("/ignore1", "/ignore2");
+ * }
+ * 
+ * + * @author Eleftheria Stein + * @since 5.4 + */ +@FunctionalInterface +public interface WebSecurityCustomizer { + + /** + * Performs the customizations on {@link WebSecurity}. + * @param web the instance of {@link WebSecurity} to apply to customizations to + */ + void customize(WebSecurity web); + +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java index 9f8527e3a49..6b8c58b8813 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java @@ -256,6 +256,76 @@ public void loadConfigWhenBothAdapterAndFilterChainConfiguredThenException() { } + @Test + public void loadConfigWhenOnlyWebSecurityCustomizerThenDefaultFilterChainCreated() { + this.spring.register(WebSecurityCustomizerConfig.class).autowire(); + FilterChainProxy filterChainProxy = this.spring.getContext().getBean(FilterChainProxy.class); + List filterChains = filterChainProxy.getFilterChains(); + assertThat(filterChains).hasSize(3); + MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); + request.setServletPath("/ignore1"); + assertThat(filterChains.get(0).matches(request)).isTrue(); + assertThat(filterChains.get(0).getFilters()).isEmpty(); + request.setServletPath("/ignore2"); + assertThat(filterChains.get(1).matches(request)).isTrue(); + assertThat(filterChains.get(1).getFilters()).isEmpty(); + request.setServletPath("/test/**"); + assertThat(filterChains.get(2).matches(request)).isTrue(); + } + + @Test + public void loadConfigWhenWebSecurityCustomizerAndFilterChainThenFilterChainsOrdered() { + this.spring.register(CustomizerAndFilterChainConfig.class).autowire(); + FilterChainProxy filterChainProxy = this.spring.getContext().getBean(FilterChainProxy.class); + List filterChains = filterChainProxy.getFilterChains(); + assertThat(filterChains).hasSize(3); + MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); + request.setServletPath("/ignore1"); + assertThat(filterChains.get(0).matches(request)).isTrue(); + assertThat(filterChains.get(0).getFilters()).isEmpty(); + request.setServletPath("/ignore2"); + assertThat(filterChains.get(1).matches(request)).isTrue(); + assertThat(filterChains.get(1).getFilters()).isEmpty(); + request.setServletPath("/role1/**"); + assertThat(filterChains.get(2).matches(request)).isTrue(); + request.setServletPath("/test/**"); + assertThat(filterChains.get(2).matches(request)).isFalse(); + } + + @Test + public void loadConfigWhenWebSecurityCustomizerAndWebSecurityConfigurerAdapterThenFilterChainsOrdered() { + this.spring.register(CustomizerAndAdapterConfig.class).autowire(); + FilterChainProxy filterChainProxy = this.spring.getContext().getBean(FilterChainProxy.class); + List filterChains = filterChainProxy.getFilterChains(); + assertThat(filterChains).hasSize(3); + MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); + request.setServletPath("/ignore1"); + assertThat(filterChains.get(0).matches(request)).isTrue(); + assertThat(filterChains.get(0).getFilters()).isEmpty(); + request.setServletPath("/ignore2"); + assertThat(filterChains.get(1).matches(request)).isTrue(); + assertThat(filterChains.get(1).getFilters()).isEmpty(); + request.setServletPath("/role1/**"); + assertThat(filterChains.get(2).matches(request)).isTrue(); + request.setServletPath("/test/**"); + assertThat(filterChains.get(2).matches(request)).isFalse(); + } + + @Test + public void loadConfigWhenCustomizerAndAdapterConfigureWebSecurityThenBothConfigurationsApplied() { + this.spring.register(CustomizerAndAdapterIgnoringConfig.class).autowire(); + FilterChainProxy filterChainProxy = this.spring.getContext().getBean(FilterChainProxy.class); + List filterChains = filterChainProxy.getFilterChains(); + assertThat(filterChains).hasSize(3); + MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); + request.setServletPath("/ignore1"); + assertThat(filterChains.get(0).matches(request)).isTrue(); + assertThat(filterChains.get(0).getFilters()).isEmpty(); + request.setServletPath("/ignore2"); + assertThat(filterChains.get(1).matches(request)).isTrue(); + assertThat(filterChains.get(1).getFilters()).isEmpty(); + } + @EnableWebSecurity @Import(AuthenticationTestConfiguration.class) static class SortedWebSecurityConfigurerAdaptersConfig { @@ -682,4 +752,86 @@ protected void configure(HttpSecurity http) throws Exception { } + @EnableWebSecurity + @Import(AuthenticationTestConfiguration.class) + static class WebSecurityCustomizerConfig { + + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { + return (web) -> web.ignoring().antMatchers("/ignore1", "/ignore2"); + } + + } + + @EnableWebSecurity + @Import(AuthenticationTestConfiguration.class) + static class CustomizerAndFilterChainConfig { + + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { + return (web) -> web.ignoring().antMatchers("/ignore1", "/ignore2"); + } + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + return http + .antMatcher("/role1/**") + .authorizeRequests((authorize) -> authorize + .anyRequest().hasRole("1") + ) + .build(); + // @formatter:on + } + + } + + @EnableWebSecurity + @Import(AuthenticationTestConfiguration.class) + static class CustomizerAndAdapterConfig { + + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { + return (web) -> web.ignoring().antMatchers("/ignore1", "/ignore2"); + } + + @Configuration + static class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .antMatcher("/role1/**") + .authorizeRequests((authorize) -> authorize + .anyRequest().hasRole("1") + ); + // @formatter:on + } + + } + + } + + @EnableWebSecurity + @Import(AuthenticationTestConfiguration.class) + static class CustomizerAndAdapterIgnoringConfig { + + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { + return (web) -> web.ignoring().antMatchers("/ignore1"); + } + + @Configuration + static class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Override + public void configure(WebSecurity web) throws Exception { + web.ignoring().antMatchers("/ignore2"); + } + + } + + } + }