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:
parent
4855290a76
commit
1ff5eb6b57
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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> {
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 { }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue