Skip to content

Commit 1ff5eb6

Browse files
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
1 parent 4855290 commit 1ff5eb6

File tree

7 files changed

+304
-15
lines changed

7 files changed

+304
-15
lines changed

config/src/main/java/org/springframework/security/config/annotation/AbstractConfiguredSecurityBuilder.java

+19-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2013 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -27,6 +27,7 @@
2727
import org.apache.commons.logging.Log;
2828
import org.apache.commons.logging.LogFactory;
2929

30+
import org.springframework.security.config.Customizer;
3031
import org.springframework.security.config.annotation.web.builders.WebSecurity;
3132
import org.springframework.util.Assert;
3233
import org.springframework.web.filter.DelegatingFilterProxy;
@@ -139,6 +140,23 @@ public <C extends SecurityConfigurer<O, B>> C apply(C configurer) throws Excepti
139140
return configurer;
140141
}
141142

143+
/**
144+
* Applies a {@link SecurityConfigurerAdapter} to this {@link SecurityBuilder} and
145+
* invokes {@link SecurityConfigurerAdapter#setBuilder(SecurityBuilder)}.
146+
* @param configurer
147+
* @return the {@link SecurityBuilder} for further customizations
148+
* @throws Exception
149+
* @since 6.2
150+
*/
151+
@SuppressWarnings("unchecked")
152+
public <C extends SecurityConfigurerAdapter<O, B>> B with(C configurer, Customizer<C> customizer) throws Exception {
153+
configurer.addObjectPostProcessor(this.objectPostProcessor);
154+
configurer.setBuilder((B) this);
155+
add(configurer);
156+
customizer.customize(configurer);
157+
return (B) this;
158+
}
159+
142160
/**
143161
* Sets an object that is shared by multiple {@link SecurityConfigurer}.
144162
* @param sharedType the Class to key the shared object by.

config/src/main/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDsl.kt

+34-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,9 @@
1616

1717
package org.springframework.security.config.annotation.web
1818

19+
import jakarta.servlet.Filter
20+
import jakarta.servlet.http.HttpServletRequest
21+
import org.checkerframework.checker.units.qual.C
1922
import org.springframework.context.ApplicationContext
2023
import org.springframework.security.authentication.AuthenticationManager
2124
import org.springframework.security.config.annotation.SecurityConfigurerAdapter
@@ -24,9 +27,6 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
2427
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository
2528
import org.springframework.security.web.DefaultSecurityFilterChain
2629
import org.springframework.security.web.util.matcher.RequestMatcher
27-
import org.springframework.util.ClassUtils
28-
import jakarta.servlet.Filter
29-
import jakarta.servlet.http.HttpServletRequest
3030

3131
/**
3232
* Configures [HttpSecurity] using a [HttpSecurity Kotlin DSL][HttpSecurityDsl].
@@ -107,6 +107,36 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu
107107
return this.http.apply(configurer).apply(configuration)
108108
}
109109

110+
/**
111+
* Applies a [SecurityConfigurerAdapter] to this [HttpSecurity]
112+
*
113+
* Example:
114+
*
115+
* ```
116+
* @Configuration
117+
* @EnableWebSecurity
118+
* class SecurityConfig {
119+
*
120+
* @Bean
121+
* fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
122+
* http {
123+
* with(CustomSecurityConfigurer<HttpSecurity>()) {
124+
* customProperty = "..."
125+
* }
126+
* }
127+
* return http.build()
128+
* }
129+
* }
130+
* ```
131+
*
132+
* @param configurer
133+
* the [HttpSecurity] for further customizations
134+
* @since 6.2
135+
*/
136+
fun <C : SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>> with(configurer: C, configuration: C.() -> Unit = { }): HttpSecurity? {
137+
return this.http.with(configurer, configuration)
138+
}
139+
110140
/**
111141
* Allows configuring the [HttpSecurity] to only be invoked when matching the
112142
* provided pattern.

config/src/test/java/org/springframework/security/config/annotation/web/AbstractConfiguredSecurityBuilderTests.java

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@
2121
import org.junit.jupiter.api.BeforeEach;
2222
import org.junit.jupiter.api.Test;
2323

24+
import org.springframework.security.config.Customizer;
2425
import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder;
2526
import org.springframework.security.config.annotation.ObjectPostProcessor;
2627
import org.springframework.security.config.annotation.SecurityConfigurer;
@@ -149,6 +150,19 @@ public void getConfigurersWhenMultipleConfigurersThenConfigurersReturned() throw
149150
assertThat(builder.getConfigurers(DelegateSecurityConfigurer.class)).hasSize(2);
150151
}
151152

153+
@Test
154+
public void withWhenConfigurerThenConfigurerAdded() throws Exception {
155+
this.builder.with(new TestSecurityConfigurer(), Customizer.withDefaults());
156+
assertThat(this.builder.getConfigurers(TestSecurityConfigurer.class)).hasSize(1);
157+
}
158+
159+
@Test
160+
public void withWhenDuplicateConfigurerAddedThenDuplicateConfigurerRemoved() throws Exception {
161+
this.builder.with(new TestSecurityConfigurer(), Customizer.withDefaults());
162+
this.builder.with(new TestSecurityConfigurer(), Customizer.withDefaults());
163+
assertThat(this.builder.getConfigurers(TestSecurityConfigurer.class)).hasSize(1);
164+
}
165+
152166
private static class ApplyAndRemoveSecurityConfigurer
153167
extends SecurityConfigurerAdapter<Object, TestConfiguredSecurityBuilder> {
154168

config/src/test/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfigurationTests.java

+66
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import org.springframework.security.authentication.event.AbstractAuthenticationEvent;
4848
import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent;
4949
import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
50+
import org.springframework.security.config.Customizer;
5051
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
5152
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
5253
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
@@ -63,6 +64,7 @@
6364
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
6465
import org.springframework.security.test.web.servlet.RequestCacheResultMatcher;
6566
import org.springframework.security.web.SecurityFilterChain;
67+
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
6668
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
6769
import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter;
6870
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
@@ -90,6 +92,8 @@
9092
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
9193
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
9294
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
95+
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
96+
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated;
9397
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
9498
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
9599
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@@ -365,6 +369,27 @@ public void configureWhenCorsConfigurationSourceThenApplyCors() {
365369
assertThat(configSource).isInstanceOf(UrlBasedCorsConfigurationSource.class);
366370
}
367371

372+
@Test
373+
public void configureWhenAddingCustomDslUsingWithThenApplied() throws Exception {
374+
this.spring.register(WithCustomDslConfig.class, UserDetailsConfig.class).autowire();
375+
SecurityFilterChain filterChain = this.spring.getContext().getBean(SecurityFilterChain.class);
376+
List<Filter> filters = filterChain.getFilters();
377+
assertThat(filters).hasAtLeastOneElementOfType(UsernamePasswordAuthenticationFilter.class);
378+
this.mockMvc.perform(formLogin()).andExpectAll(redirectedUrl("/"), authenticated());
379+
}
380+
381+
@Test
382+
public void configureWhenCustomDslAddedFromFactoriesAndDisablingUsingWithThenNotApplied() throws Exception {
383+
this.springFactoriesLoader.when(
384+
() -> SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, getClass().getClassLoader()))
385+
.thenReturn(List.of(new WithCustomDsl()));
386+
this.spring.register(WithCustomDslDisabledConfig.class, UserDetailsConfig.class).autowire();
387+
SecurityFilterChain filterChain = this.spring.getContext().getBean(SecurityFilterChain.class);
388+
List<Filter> filters = filterChain.getFilters();
389+
assertThat(filters).doesNotHaveAnyElementsOfTypes(UsernamePasswordAuthenticationFilter.class);
390+
this.mockMvc.perform(formLogin()).andExpectAll(status().isNotFound(), unauthenticated());
391+
}
392+
368393
@RestController
369394
static class NameController {
370395

@@ -661,4 +686,45 @@ public void configure(HttpSecurity builder) {
661686

662687
}
663688

689+
@Configuration
690+
@EnableWebSecurity
691+
static class WithCustomDslConfig {
692+
693+
@Bean
694+
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
695+
// @formatter:off
696+
http
697+
.with(new WithCustomDsl(), Customizer.withDefaults())
698+
.httpBasic(Customizer.withDefaults());
699+
// @formatter:on
700+
return http.build();
701+
}
702+
703+
}
704+
705+
@Configuration
706+
@EnableWebSecurity
707+
static class WithCustomDslDisabledConfig {
708+
709+
@Bean
710+
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
711+
// @formatter:off
712+
http
713+
.with(new WithCustomDsl(), (dsl) -> dsl.disable())
714+
.httpBasic(Customizer.withDefaults());
715+
// @formatter:on
716+
return http.build();
717+
}
718+
719+
}
720+
721+
static class WithCustomDsl extends AbstractHttpConfigurer<WithCustomDsl, HttpSecurity> {
722+
723+
@Override
724+
public void init(HttpSecurity builder) throws Exception {
725+
builder.formLogin(Customizer.withDefaults());
726+
}
727+
728+
}
729+
664730
}

config/src/test/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDslTests.kt

+71-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@ package org.springframework.security.config.annotation.web
1919
import io.mockk.every
2020
import io.mockk.mockkObject
2121
import io.mockk.verify
22+
import jakarta.servlet.Filter
2223
import org.assertj.core.api.Assertions.assertThat
2324
import org.junit.jupiter.api.Test
2425
import org.junit.jupiter.api.extension.ExtendWith
@@ -55,7 +56,6 @@ import org.springframework.test.web.servlet.get
5556
import org.springframework.test.web.servlet.post
5657
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
5758
import org.springframework.web.servlet.config.annotation.EnableWebMvc
58-
import jakarta.servlet.Filter
5959

6060
/**
6161
* Tests for [HttpSecurityDsl]
@@ -530,6 +530,18 @@ class HttpSecurityDslTests {
530530
)
531531
}
532532

533+
@Test
534+
fun `HTTP security when apply custom security configurer using with then custom filter added to filter chain`() {
535+
this.spring.register(CustomSecurityConfigurerConfig::class.java).autowire()
536+
537+
val filterChain = spring.context.getBean(FilterChainProxy::class.java)
538+
val filterClasses: List<Class<out Filter>> = filterChain.getFilters("/").map { it.javaClass }
539+
540+
assertThat(filterClasses).contains(
541+
CustomFilter::class.java
542+
)
543+
}
544+
533545
@Configuration
534546
@EnableWebSecurity
535547
@EnableWebMvc
@@ -545,6 +557,21 @@ class HttpSecurityDslTests {
545557
}
546558
}
547559

560+
@Configuration
561+
@EnableWebSecurity
562+
@EnableWebMvc
563+
open class CustomSecurityConfigurerUsingWithConfig {
564+
@Bean
565+
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
566+
http {
567+
with(CustomSecurityConfigurer<HttpSecurity>()) {
568+
filter = CustomFilter()
569+
}
570+
}
571+
return http.build()
572+
}
573+
}
574+
548575
class CustomSecurityConfigurer<H : HttpSecurityBuilder<H>> : AbstractHttpConfigurer<CustomSecurityConfigurer<H>, H>() {
549576
var filter: Filter? = null
550577
override fun init(builder: H) {
@@ -555,4 +582,46 @@ class HttpSecurityDslTests {
555582
builder.addFilterBefore(CustomFilter(), UsernamePasswordAuthenticationFilter::class.java)
556583
}
557584
}
585+
586+
@Test
587+
fun `HTTP security when apply form login using with from custom security configurer then filter added to filter chain`() {
588+
this.spring.register(CustomDslUsingWithConfig::class.java).autowire()
589+
590+
val filterChain = spring.context.getBean(FilterChainProxy::class.java)
591+
val filterClasses: List<Class<out Filter>> = filterChain.getFilters("/").map { it.javaClass }
592+
593+
assertThat(filterClasses).contains(
594+
UsernamePasswordAuthenticationFilter::class.java
595+
)
596+
}
597+
598+
@Configuration
599+
@EnableWebSecurity
600+
@EnableWebMvc
601+
open class CustomDslUsingWithConfig {
602+
@Bean
603+
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
604+
http {
605+
with(CustomDslFormLogin()) {
606+
formLogin = true
607+
}
608+
httpBasic { }
609+
}
610+
return http.build()
611+
}
612+
}
613+
614+
class CustomDslFormLogin: AbstractHttpConfigurer<CustomDslFormLogin, HttpSecurity>() {
615+
616+
var formLogin = false
617+
618+
override fun init(builder: HttpSecurity) {
619+
if (formLogin) {
620+
builder.formLogin { }
621+
}
622+
}
623+
624+
}
625+
626+
558627
}

0 commit comments

Comments
 (0)