Explorar o código

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 %!s(int64=2) %!d(string=hai) anos
pai
achega
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