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
This commit is contained in:
Marcus Da Coregio 2023-06-26 10:52:05 -03:00 committed by Marcus Hert Da Coregio
parent 4855290a76
commit 1ff5eb6b57
7 changed files with 304 additions and 15 deletions

View File

@ -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.

View File

@ -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.

View File

@ -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> {

View File

@ -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());
}
}
}

View File

@ -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 { }
}
}
}
}

View File

@ -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

View File

@ -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