Skip to content

Commit 5dc44fb

Browse files
committed
Auto-exclude conflicting Spring Boot security auto-configurations
Add SecurityAutoConfigurationExcluder implementing AutoConfigurationImportFilter to automatically exclude 7 Spring Boot security auto-configuration classes that conflict with the Grails Spring Security plugin. Previously, every Grails 7 user had to manually add spring.autoconfigure.exclude entries to application.yml (documented in README). This filter eliminates that requirement by filtering them out during Spring Boot's auto-configuration discovery phase, before bytecode is loaded. Excluded auto-configurations: - SecurityAutoConfiguration - SecurityFilterAutoConfiguration - UserDetailsServiceAutoConfiguration - OAuth2ClientAutoConfiguration (2 packages) - OAuth2ResourceServerAutoConfiguration - ManagementWebSecurityAutoConfiguration Implementation: - SecurityAutoConfigurationExcluder.groovy — the filter - META-INF/spring.factories — SPI registration - build.gradle — compileOnly spring-boot-autoconfigure - SecurityAutoConfigurationExcluderSpec — 18 Spock tests
1 parent f899ecd commit 5dc44fb

4 files changed

Lines changed: 286 additions & 0 deletions

File tree

plugin-core/plugin/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ dependencies {
7777
compileOnly 'jline:jline' // for shell commands
7878
compileOnly 'org.apache.groovy:groovy' // Compile-time annotations
7979
compileOnly 'jakarta.servlet:jakarta.servlet-api' // Provided
80+
compileOnly 'org.springframework.boot:spring-boot-autoconfigure' // AutoConfigurationImportFilter SPI
8081
compileOnly 'org.slf4j:slf4j-nop' // Prevents warnings about missing slf4j implementation during compilation
8182

8283
runtimeOnly 'org.apache.grails:grails-services' // A service is defined in the plugin
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* https://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package grails.plugin.springsecurity
20+
21+
import groovy.transform.CompileStatic
22+
import org.springframework.boot.autoconfigure.AutoConfigurationImportFilter
23+
import org.springframework.boot.autoconfigure.AutoConfigurationMetadata
24+
25+
/**
26+
* Automatically excludes Spring Boot security auto-configuration classes that
27+
* conflict with the Grails Spring Security plugin.
28+
*
29+
* <p>When the Grails Spring Security plugin is on the classpath, Spring Boot's
30+
* security auto-configurations (e.g. {@code SecurityAutoConfiguration},
31+
* {@code SecurityFilterAutoConfiguration}) create duplicate
32+
* {@code SecurityFilterChain} beans and other security infrastructure that
33+
* conflicts with the plugin's own bean definitions in
34+
* {@link SpringSecurityCoreGrailsPlugin#doWithSpring}.</p>
35+
*
36+
* <p>Previously, users had to manually exclude up to 7 auto-configuration classes
37+
* in {@code application.yml}. This filter removes that requirement by
38+
* automatically filtering them out during Spring Boot's auto-configuration
39+
* discovery phase.</p>
40+
*
41+
* <p>Registered via {@code META-INF/spring.factories} as an
42+
* {@link AutoConfigurationImportFilter}. This runs before auto-configuration
43+
* bytecode is loaded, so there is no performance overhead from excluded classes.</p>
44+
*
45+
* @since 7.0.2
46+
* @see AutoConfigurationImportFilter
47+
*/
48+
@CompileStatic
49+
class SecurityAutoConfigurationExcluder implements AutoConfigurationImportFilter {
50+
51+
/**
52+
* Spring Boot security auto-configuration classes that conflict with the
53+
* Grails Spring Security plugin. These are excluded unconditionally when the
54+
* plugin is on the classpath.
55+
*
56+
* <ul>
57+
* <li>{@code SecurityAutoConfiguration} — creates a default {@code SecurityFilterChain}
58+
* that conflicts with the plugin's {@code FilterChainProxy}</li>
59+
* <li>{@code SecurityFilterAutoConfiguration} — registers a
60+
* {@code DelegatingFilterProxyRegistrationBean} that duplicates the plugin's
61+
* {@code springSecurityFilterChainRegistrationBean}</li>
62+
* <li>{@code UserDetailsServiceAutoConfiguration} — creates an in-memory
63+
* {@code UserDetailsService} that conflicts with the plugin's
64+
* {@code GormUserDetailsService}</li>
65+
* <li>{@code OAuth2ClientAutoConfiguration} (servlet) — conflicts when the
66+
* plugin-oauth2 module manages OAuth2 configuration</li>
67+
* <li>{@code OAuth2ResourceServerAutoConfiguration} — conflicts with the
68+
* plugin's resource server security setup</li>
69+
* <li>{@code ManagementWebSecurityAutoConfiguration} — Actuator security
70+
* that conflicts when Actuator is on the classpath</li>
71+
* </ul>
72+
*/
73+
private static final Set<String> EXCLUDED_AUTO_CONFIGURATIONS = [
74+
'org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration',
75+
'org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration',
76+
'org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration',
77+
'org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration',
78+
'org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientAutoConfiguration',
79+
'org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration',
80+
'org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration',
81+
] as Set<String>
82+
83+
@Override
84+
boolean[] match(String[] autoConfigurationClasses, AutoConfigurationMetadata autoConfigurationMetadata) {
85+
boolean[] matches = new boolean[autoConfigurationClasses.length]
86+
for (int i = 0; i < autoConfigurationClasses.length; i++) {
87+
matches[i] = !EXCLUDED_AUTO_CONFIGURATIONS.contains(autoConfigurationClasses[i])
88+
}
89+
return matches
90+
}
91+
92+
/**
93+
* Returns the set of auto-configuration class names that this filter excludes.
94+
* Exposed for testing and diagnostic purposes.
95+
*
96+
* @return unmodifiable set of excluded class names
97+
*/
98+
static Set<String> getExcludedAutoConfigurations() {
99+
return Collections.unmodifiableSet(EXCLUDED_AUTO_CONFIGURATIONS)
100+
}
101+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one or more
2+
# contributor license agreements. See the NOTICE file distributed with
3+
# this work for additional information regarding copyright ownership.
4+
# The ASF licenses this file to You under the Apache License, Version 2.0
5+
# (the "License"); you may not use this file except in compliance with
6+
# the License. You may obtain a copy of the License at
7+
#
8+
# https://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
# Automatically exclude Spring Boot security auto-configurations that conflict
17+
# with the Grails Spring Security plugin's bean definitions.
18+
# See: SecurityAutoConfigurationExcluder javadoc for the full list and rationale.
19+
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
20+
grails.plugin.springsecurity.SecurityAutoConfigurationExcluder
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* https://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package grails.plugin.springsecurity
20+
21+
import spock.lang.Specification
22+
import spock.lang.Subject
23+
import spock.lang.Unroll
24+
25+
/**
26+
* Tests for {@link SecurityAutoConfigurationExcluder}.
27+
*
28+
* Verifies that Spring Boot security auto-configuration classes that conflict
29+
* with the Grails Spring Security plugin are filtered out during the
30+
* auto-configuration discovery phase.
31+
*/
32+
class SecurityAutoConfigurationExcluderSpec extends Specification {
33+
34+
@Subject
35+
SecurityAutoConfigurationExcluder excluder = new SecurityAutoConfigurationExcluder()
36+
37+
@Unroll
38+
def "match excludes conflicting auto-configuration: #className"() {
39+
given:
40+
String[] autoConfigs = [className] as String[]
41+
42+
when:
43+
boolean[] results = excluder.match(autoConfigs, null)
44+
45+
then: 'the conflicting auto-configuration is excluded (false = filtered out)'
46+
!results[0]
47+
48+
where:
49+
className << [
50+
'org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration',
51+
'org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration',
52+
'org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration',
53+
'org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration',
54+
'org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientAutoConfiguration',
55+
'org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration',
56+
'org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration',
57+
]
58+
}
59+
60+
@Unroll
61+
def "match preserves non-security auto-configuration: #className"() {
62+
given:
63+
String[] autoConfigs = [className] as String[]
64+
65+
when:
66+
boolean[] results = excluder.match(autoConfigs, null)
67+
68+
then: 'non-security auto-configurations pass through (true = included)'
69+
results[0]
70+
71+
where:
72+
className << [
73+
'org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration',
74+
'org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration',
75+
'org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration',
76+
'org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration',
77+
'org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration',
78+
]
79+
}
80+
81+
def "match handles mixed array of included and excluded auto-configurations"() {
82+
given:
83+
String[] autoConfigs = [
84+
'org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration',
85+
'org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration',
86+
'org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration',
87+
'org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration',
88+
'org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration',
89+
] as String[]
90+
91+
when:
92+
boolean[] results = excluder.match(autoConfigs, null)
93+
94+
then:
95+
results[0] // DataSource — included
96+
!results[1] // SecurityAutoConfiguration — excluded
97+
results[2] // Jackson — included
98+
!results[3] // SecurityFilterAutoConfiguration — excluded
99+
results[4] // DispatcherServlet — included
100+
}
101+
102+
def "match handles empty array"() {
103+
given:
104+
String[] autoConfigs = [] as String[]
105+
106+
when:
107+
boolean[] results = excluder.match(autoConfigs, null)
108+
109+
then:
110+
results.length == 0
111+
}
112+
113+
def "match handles null metadata parameter gracefully"() {
114+
given: 'autoConfigurationMetadata is null (not used by this filter)'
115+
String[] autoConfigs = [
116+
'org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration',
117+
] as String[]
118+
119+
when:
120+
boolean[] results = excluder.match(autoConfigs, null)
121+
122+
then: 'still works correctly'
123+
!results[0]
124+
}
125+
126+
def "getExcludedAutoConfigurations returns all 7 known conflicting classes"() {
127+
when:
128+
Set<String> excluded = SecurityAutoConfigurationExcluder.excludedAutoConfigurations
129+
130+
then:
131+
excluded.size() == 7
132+
excluded.contains('org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration')
133+
excluded.contains('org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration')
134+
excluded.contains('org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration')
135+
excluded.contains('org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration')
136+
excluded.contains('org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientAutoConfiguration')
137+
excluded.contains('org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration')
138+
excluded.contains('org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration')
139+
}
140+
141+
def "getExcludedAutoConfigurations returns unmodifiable set"() {
142+
when:
143+
Set<String> excluded = SecurityAutoConfigurationExcluder.excludedAutoConfigurations
144+
excluded.add('some.new.AutoConfiguration')
145+
146+
then:
147+
thrown(UnsupportedOperationException)
148+
}
149+
150+
def "spring.factories registers the filter correctly"() {
151+
when: 'reading the spring.factories resource from the classpath'
152+
URL resource = getClass().getClassLoader().getResource('META-INF/spring.factories')
153+
154+
then: 'the resource exists'
155+
resource != null
156+
157+
when:
158+
String content = resource.text
159+
160+
then: 'it registers SecurityAutoConfigurationExcluder as an AutoConfigurationImportFilter'
161+
content.contains('org.springframework.boot.autoconfigure.AutoConfigurationImportFilter')
162+
content.contains('grails.plugin.springsecurity.SecurityAutoConfigurationExcluder')
163+
}
164+
}

0 commit comments

Comments
 (0)