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");
+ }
+
+ }
+
+ }
+
}