Просмотр исходного кода

Add with() method to apply SecurityConfigurerAdapter

This method is intended to replace .apply() because it will not be possible to chain configurations when .and() gets removed

Closes gh-13204
Marcus Da Coregio 2 лет назад
Родитель
Сommit
1ff5eb6b57

+ 19 - 1
config/src/main/java/org/springframework/security/config/annotation/AbstractConfiguredSecurityBuilder.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2013 the original author or authors.
+ * Copyright 2002-2023 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.
@@ -27,6 +27,7 @@ import java.util.Map;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
+import org.springframework.security.config.Customizer;
 import org.springframework.security.config.annotation.web.builders.WebSecurity;
 import org.springframework.util.Assert;
 import org.springframework.web.filter.DelegatingFilterProxy;
@@ -139,6 +140,23 @@ public abstract class AbstractConfiguredSecurityBuilder<O, B extends SecurityBui
 		return configurer;
 	}
 
+	/**
+	 * Applies a {@link SecurityConfigurerAdapter} to this {@link SecurityBuilder} and
+	 * invokes {@link SecurityConfigurerAdapter#setBuilder(SecurityBuilder)}.
+	 * @param configurer
+	 * @return the {@link SecurityBuilder} for further customizations
+	 * @throws Exception
+	 * @since 6.2
+	 */
+	@SuppressWarnings("unchecked")
+	public <C extends SecurityConfigurerAdapter<O, B>> B with(C configurer, Customizer<C> customizer) throws Exception {
+		configurer.addObjectPostProcessor(this.objectPostProcessor);
+		configurer.setBuilder((B) this);
+		add(configurer);
+		customizer.customize(configurer);
+		return (B) this;
+	}
+
 	/**
 	 * Sets an object that is shared by multiple {@link SecurityConfigurer}.
 	 * @param sharedType the Class to key the shared object by.

+ 34 - 4
config/src/main/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDsl.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2023 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,9 @@
 
 package org.springframework.security.config.annotation.web
 
+import jakarta.servlet.Filter
+import jakarta.servlet.http.HttpServletRequest
+import org.checkerframework.checker.units.qual.C
 import org.springframework.context.ApplicationContext
 import org.springframework.security.authentication.AuthenticationManager
 import org.springframework.security.config.annotation.SecurityConfigurerAdapter
@@ -24,9 +27,6 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
 import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository
 import org.springframework.security.web.DefaultSecurityFilterChain
 import org.springframework.security.web.util.matcher.RequestMatcher
-import org.springframework.util.ClassUtils
-import jakarta.servlet.Filter
-import jakarta.servlet.http.HttpServletRequest
 
 /**
  * Configures [HttpSecurity] using a [HttpSecurity Kotlin DSL][HttpSecurityDsl].
@@ -107,6 +107,36 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu
         return this.http.apply(configurer).apply(configuration)
     }
 
+    /**
+     * Applies a [SecurityConfigurerAdapter] to this [HttpSecurity]
+     *
+     * Example:
+     *
+     * ```
+     * @Configuration
+     * @EnableWebSecurity
+     * class SecurityConfig {
+     *
+     *     @Bean
+     *     fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+     *         http {
+     *             with(CustomSecurityConfigurer<HttpSecurity>()) {
+     *                 customProperty = "..."
+     *             }
+     *         }
+     *         return http.build()
+     *     }
+     * }
+     * ```
+     *
+     * @param configurer
+     * the [HttpSecurity] for further customizations
+     * @since 6.2
+     */
+    fun <C : SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>> with(configurer: C, configuration: C.() -> Unit = { }): HttpSecurity? {
+        return this.http.with(configurer, configuration)
+    }
+
     /**
      * Allows configuring the [HttpSecurity] to only be invoked when matching the
      * provided pattern.

+ 15 - 1
config/src/test/java/org/springframework/security/config/annotation/web/AbstractConfiguredSecurityBuilderTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2018 the original author or authors.
+ * Copyright 2002-2023 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.
@@ -21,6 +21,7 @@ import java.util.List;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
+import org.springframework.security.config.Customizer;
 import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder;
 import org.springframework.security.config.annotation.ObjectPostProcessor;
 import org.springframework.security.config.annotation.SecurityConfigurer;
@@ -149,6 +150,19 @@ public class AbstractConfiguredSecurityBuilderTests {
 		assertThat(builder.getConfigurers(DelegateSecurityConfigurer.class)).hasSize(2);
 	}
 
+	@Test
+	public void withWhenConfigurerThenConfigurerAdded() throws Exception {
+		this.builder.with(new TestSecurityConfigurer(), Customizer.withDefaults());
+		assertThat(this.builder.getConfigurers(TestSecurityConfigurer.class)).hasSize(1);
+	}
+
+	@Test
+	public void withWhenDuplicateConfigurerAddedThenDuplicateConfigurerRemoved() throws Exception {
+		this.builder.with(new TestSecurityConfigurer(), Customizer.withDefaults());
+		this.builder.with(new TestSecurityConfigurer(), Customizer.withDefaults());
+		assertThat(this.builder.getConfigurers(TestSecurityConfigurer.class)).hasSize(1);
+	}
+
 	private static class ApplyAndRemoveSecurityConfigurer
 			extends SecurityConfigurerAdapter<Object, TestConfiguredSecurityBuilder> {
 

+ 66 - 0
config/src/test/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfigurationTests.java

@@ -47,6 +47,7 @@ import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.authentication.event.AbstractAuthenticationEvent;
 import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent;
 import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
+import org.springframework.security.config.Customizer;
 import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
@@ -63,6 +64,7 @@ import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.provisioning.InMemoryUserDetailsManager;
 import org.springframework.security.test.web.servlet.RequestCacheResultMatcher;
 import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
 import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
 import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter;
 import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
@@ -90,6 +92,8 @@ import static org.springframework.security.test.web.servlet.request.SecurityMock
 import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
 import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
 import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
+import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
+import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@@ -365,6 +369,27 @@ public class HttpSecurityConfigurationTests {
 		assertThat(configSource).isInstanceOf(UrlBasedCorsConfigurationSource.class);
 	}
 
+	@Test
+	public void configureWhenAddingCustomDslUsingWithThenApplied() throws Exception {
+		this.spring.register(WithCustomDslConfig.class, UserDetailsConfig.class).autowire();
+		SecurityFilterChain filterChain = this.spring.getContext().getBean(SecurityFilterChain.class);
+		List<Filter> filters = filterChain.getFilters();
+		assertThat(filters).hasAtLeastOneElementOfType(UsernamePasswordAuthenticationFilter.class);
+		this.mockMvc.perform(formLogin()).andExpectAll(redirectedUrl("/"), authenticated());
+	}
+
+	@Test
+	public void configureWhenCustomDslAddedFromFactoriesAndDisablingUsingWithThenNotApplied() throws Exception {
+		this.springFactoriesLoader.when(
+				() -> SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, getClass().getClassLoader()))
+				.thenReturn(List.of(new WithCustomDsl()));
+		this.spring.register(WithCustomDslDisabledConfig.class, UserDetailsConfig.class).autowire();
+		SecurityFilterChain filterChain = this.spring.getContext().getBean(SecurityFilterChain.class);
+		List<Filter> filters = filterChain.getFilters();
+		assertThat(filters).doesNotHaveAnyElementsOfTypes(UsernamePasswordAuthenticationFilter.class);
+		this.mockMvc.perform(formLogin()).andExpectAll(status().isNotFound(), unauthenticated());
+	}
+
 	@RestController
 	static class NameController {
 
@@ -661,4 +686,45 @@ public class HttpSecurityConfigurationTests {
 
 	}
 
+	@Configuration
+	@EnableWebSecurity
+	static class WithCustomDslConfig {
+
+		@Bean
+		SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+					.with(new WithCustomDsl(), Customizer.withDefaults())
+					.httpBasic(Customizer.withDefaults());
+			// @formatter:on
+			return http.build();
+		}
+
+	}
+
+	@Configuration
+	@EnableWebSecurity
+	static class WithCustomDslDisabledConfig {
+
+		@Bean
+		SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+					.with(new WithCustomDsl(), (dsl) -> dsl.disable())
+					.httpBasic(Customizer.withDefaults());
+			// @formatter:on
+			return http.build();
+		}
+
+	}
+
+	static class WithCustomDsl extends AbstractHttpConfigurer<WithCustomDsl, HttpSecurity> {
+
+		@Override
+		public void init(HttpSecurity builder) throws Exception {
+			builder.formLogin(Customizer.withDefaults());
+		}
+
+	}
+
 }

+ 71 - 2
config/src/test/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDslTests.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2023 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.
@@ -19,6 +19,7 @@ package org.springframework.security.config.annotation.web
 import io.mockk.every
 import io.mockk.mockkObject
 import io.mockk.verify
+import jakarta.servlet.Filter
 import org.assertj.core.api.Assertions.assertThat
 import org.junit.jupiter.api.Test
 import org.junit.jupiter.api.extension.ExtendWith
@@ -55,7 +56,6 @@ import org.springframework.test.web.servlet.get
 import org.springframework.test.web.servlet.post
 import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
 import org.springframework.web.servlet.config.annotation.EnableWebMvc
-import jakarta.servlet.Filter
 
 /**
  * Tests for [HttpSecurityDsl]
@@ -530,6 +530,18 @@ class HttpSecurityDslTests {
         )
     }
 
+    @Test
+    fun `HTTP security when apply custom security configurer using with then custom filter added to filter chain`() {
+        this.spring.register(CustomSecurityConfigurerConfig::class.java).autowire()
+
+        val filterChain = spring.context.getBean(FilterChainProxy::class.java)
+        val filterClasses: List<Class<out Filter>> = filterChain.getFilters("/").map { it.javaClass }
+
+        assertThat(filterClasses).contains(
+            CustomFilter::class.java
+        )
+    }
+
     @Configuration
     @EnableWebSecurity
     @EnableWebMvc
@@ -545,6 +557,21 @@ class HttpSecurityDslTests {
         }
     }
 
+    @Configuration
+    @EnableWebSecurity
+    @EnableWebMvc
+    open class CustomSecurityConfigurerUsingWithConfig {
+        @Bean
+        open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+            http {
+                with(CustomSecurityConfigurer<HttpSecurity>()) {
+                    filter = CustomFilter()
+                }
+            }
+            return http.build()
+        }
+    }
+
     class CustomSecurityConfigurer<H : HttpSecurityBuilder<H>> : AbstractHttpConfigurer<CustomSecurityConfigurer<H>, H>() {
         var filter: Filter? = null
         override fun init(builder: H) {
@@ -555,4 +582,46 @@ class HttpSecurityDslTests {
             builder.addFilterBefore(CustomFilter(), UsernamePasswordAuthenticationFilter::class.java)
         }
     }
+
+    @Test
+    fun `HTTP security when apply form login using with from custom security configurer then filter added to filter chain`() {
+        this.spring.register(CustomDslUsingWithConfig::class.java).autowire()
+
+        val filterChain = spring.context.getBean(FilterChainProxy::class.java)
+        val filterClasses: List<Class<out Filter>> = filterChain.getFilters("/").map { it.javaClass }
+
+        assertThat(filterClasses).contains(
+            UsernamePasswordAuthenticationFilter::class.java
+        )
+    }
+
+    @Configuration
+    @EnableWebSecurity
+    @EnableWebMvc
+    open class CustomDslUsingWithConfig {
+        @Bean
+        open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+            http {
+                with(CustomDslFormLogin()) {
+                    formLogin = true
+                }
+                httpBasic { }
+            }
+            return http.build()
+        }
+    }
+
+    class CustomDslFormLogin: AbstractHttpConfigurer<CustomDslFormLogin, HttpSecurity>() {
+
+        var formLogin = false
+
+        override fun init(builder: HttpSecurity) {
+            if (formLogin) {
+                builder.formLogin {  }
+            }
+        }
+
+    }
+
+
 }

+ 98 - 7
docs/modules/ROOT/pages/servlet/configuration/java.adoc

@@ -227,7 +227,11 @@ This configuration is considered after `apiFilterChain`, since it has an `@Order
 
 You can provide your own custom DSLs in Spring Security:
 
-[source,java]
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
 ----
 public class MyCustomDsl extends AbstractHttpConfigurer<MyCustomDsl, HttpSecurity> {
 	private boolean flag;
@@ -260,6 +264,38 @@ public class MyCustomDsl extends AbstractHttpConfigurer<MyCustomDsl, HttpSecurit
 }
 ----
 
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+class MyCustomDsl : AbstractHttpConfigurer<MyCustomDsl, HttpSecurity>() {
+    var flag: Boolean = false
+
+    override fun init(http: HttpSecurity) {
+        // any method that adds another configurer
+        // must be done in the init method
+        http.csrf().disable()
+    }
+
+    override fun configure(http: HttpSecurity) {
+        val context: ApplicationContext = http.getSharedObject(ApplicationContext::class.java)
+
+        // here we lookup from the ApplicationContext. You can also just create a new instance.
+        val myFilter: MyFilter = context.getBean(MyFilter::class.java)
+        myFilter.setFlag(flag)
+        http.addFilterBefore(myFilter, UsernamePasswordAuthenticationFilter::class.java)
+    }
+
+    companion object {
+        @JvmStatic
+        fun customDsl(): MyCustomDsl {
+            return MyCustomDsl()
+        }
+    }
+}
+----
+======
+
 [NOTE]
 ====
 This is actually how methods like `HttpSecurity.authorizeRequests()` are implemented.
@@ -267,7 +303,11 @@ This is actually how methods like `HttpSecurity.authorizeRequests()` are impleme
 
 You can then use the custom DSL:
 
-[source,java]
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
 ----
 @Configuration
 @EnableWebSecurity
@@ -275,15 +315,37 @@ public class Config {
 	@Bean
 	public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
 		http
-			.apply(customDsl())
+			.with(MyCustomDsl.customDsl(), (dsl) -> dsl
 				.flag(true)
-				.and()
-			...;
+			)
+			// ...
 		return http.build();
 	}
 }
 ----
 
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+@Configuration
+@EnableWebSecurity
+class Config {
+
+    @Bean
+    fun filterChain(http: HttpSecurity): SecurityFilterChain {
+        http
+            .with(MyCustomDsl.customDsl()) {
+                flag = true
+            }
+            // ...
+
+        return http.build()
+    }
+}
+----
+======
+
 The code is invoked in the following order:
 
 * Code in the `Config.configure` method is invoked
@@ -301,21 +363,50 @@ org.springframework.security.config.annotation.web.configurers.AbstractHttpConfi
 
 You can also explicit disable the default:
 
-[source,java]
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
 ----
+
 @Configuration
 @EnableWebSecurity
 public class Config {
 	@Bean
 	public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
 		http
-			.apply(customDsl()).disable()
+			.with(MyCustomDsl.customDsl(), (dsl) -> dsl
+				.disable()
+			)
 			...;
 		return http.build();
 	}
 }
 ----
 
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+@Configuration
+@EnableWebSecurity
+class Config {
+
+    @Bean
+    fun filterChain(http: HttpSecurity): SecurityFilterChain {
+        http
+            .with(MyCustomDsl.customDsl()) {
+                disable()
+            }
+            // ...
+        return http.build()
+    }
+
+}
+----
+======
+
 [[post-processing-configured-objects]]
 == Post Processing Configured Objects
 

+ 1 - 0
docs/modules/ROOT/pages/whats-new.adoc

@@ -7,3 +7,4 @@ Below are the highlights of the release.
 == Configuration
 
 * https://github.com/spring-projects/spring-security/issues/5011[gh-5011] - xref:servlet/integrations/cors.adoc[(docs)] Automatically enable `.cors()` if `CorsConfigurationSource` bean is present
+* https://github.com/spring-projects/spring-security/issues/13204[gh-13204] - xref:servlet/integrations/cors.adoc[(docs)] Add `AbstractConfiguredSecurityBuilder.with(...)` method to apply configurers returning the builder