Issue
Working on an application that was using XML Schema-based Spring configuration only, I'm slowly introducing Java based configuration.
Unfortunately I'm not able to use Spring Security html
xml element and Java based SecurityFilterChain
configuration at the same time.
When I inspect the filterChains
list in the FilterChainProxy
, it only contains the filter chains from the xml configuration. Despite the Java method creating the SecurityFilterChain
bean being called, this filter chain is not added to the FilterChainProxy
.
If I remove the html
elements from the xml configuration, the SecurityFilterChain
from the Java configuration is added to the FilterChainProxy
and called as expected.
Spring versions I'm using:
- Spring Framework: 5.3.16
- Spring Security: 5.6.2
Here is the xml configuration and Java configuration I'm trying to use simultaneously:
<bean id="mySecurityConfig" class="com.xxx.SecurityConfiguration"/>
<http pattern="/style/**" security="none"/>
package com.xxx;
[...]
@EnableWebSecurity
@Configuration
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.antMatcher("/app/**")
.authorizeHttpRequests(authorize -> authorize
.antMatchers("/app/**").hasAuthority("ROLE_USER")
.anyRequest().denyAll()
)
.addFilterBefore(anotherFilter(), Saml2WebSsoAuthenticationFilter.class);
return http.build();
}
[...]
}
web.xml
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Did I make a mistake, is there a way to achieve what I want, or is it simply not possible ?
Solution
Edit: @rwinch solution posted here
You can combine the XML and Java Configuration and exposes it as a different bean name using something like this:
@Configuration
public class AggregateSpringSecurityConfiguration {
public static final String AGGREGATE_SPRING_SECURITY_FILTER_CHAIN_ID = "aggregateSpringSecurityFilterChain";
/**
* Provide a new FilterChainProxy that contains both XML and Java Configuration
* @param webSecurityConfiguration
* @return
* @throws Exception
*/
@Bean(AGGREGATE_SPRING_SECURITY_FILTER_CHAIN_ID)
Filter aggregateSpringSecurityFilterChain(WebSecurityConfiguration webSecurityConfiguration) throws Exception {
FilterChainProxy javaConfigFcp = (FilterChainProxy) webSecurityConfiguration.springSecurityFilterChain();
return new FilterChainProxy(javaConfigFcp.getFilterChains());
}
}
Then ensure your XML configuration picks it up:
<bean id="aggregateConfiguration" class="testapp.AggregateSpringSecurityConfiguration" />
Finally, you need to update your web.xml to use the bean named aggregateSpringSecurityFilterChain instead of springSecurityFilterChain.
<filter>
<!-- matches AggregateSpringSecurityConfiguration.AGGREGATE_SPRING_SECURITY_FILTER_CHAIN_ID -->
<filter-name>aggregateSpringSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>aggregateSpringSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Initial workaround
Until better solution, it is possible to use the workaround below (if really needed). It adds the missing SecurityFilterChain
to the FilterChainProxy
once Spring Application context is initialized.
I implemented it against Spring Security 5.6.2. Since it uses reflection, it might break after any upgrade of Spring Security version.
/**
* This class is a workaround to allow declaring SecurityFilterChain using xml
* configuration and Java configuration. It listens to the Spring Application
* context events ContextRefreshedEvent and add SecurityFilterChain beans,
* declared in the Java configuration, to the FilterChainProxy.
*/
@Configuration
public static class MissingSecurityFilterChainsInitializer {
private FilterChainProxy filterChainProxy;
private List<SecurityFilterChain> filterChains;
/**
* @param filterChainProxy
* @param myFilter
*/
public MissingSecurityFilterChainsInitializer(FilterChainProxy filterChainProxy,
List<SecurityFilterChain> myFilterChains) {
super();
this.filterChainProxy = filterChainProxy;
this.filterChains = myFilterChains;
}
/**
* Method listening to the Spring Application context initialization, detects
* missing SecurityFilterChain from the FilterChainProxy and add the missing
* chains to the FilterChainProxy.
*
* @param ctxRefreshed
*/
@EventListener
public void handleContextRefreshEvent(ContextRefreshedEvent ctxRefreshed) {
List<SecurityFilterChain> missingChains = filterChains.stream()
.filter(chain -> !filterChainProxy.getFilterChains().contains(chain)).collect(Collectors.toList());
if (missingChains.isEmpty()) {
return;
}
try {
Field filterChainsField = filterChainProxy.getClass().getDeclaredField("filterChains");
boolean accessibleStatus = filterChainsField.isAccessible();
try {
filterChainsField.setAccessible(true);
List<SecurityFilterChain> chains = (List<SecurityFilterChain>) filterChainsField
.get(filterChainProxy);
if (chains == null) {
throw new IllegalStateException(
"Unable to add the missing security filter chains to the existing filter chains list of the FilterChainProxy. The list from the FilterChainProxy is null");
}
missingChains.forEach(chain -> chains.add(chain));
} finally {
filterChainsField.setAccessible(accessibleStatus);
}
} catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) {
throw new IllegalStateException(
"Unable to add the missing security filter chains to the existing filter chains list of the FilterChainProxy: "
+ e.getMessage(),
e);
}
}
}
Answered By - Simon Hill
Answer Checked By - Clifford M. (JavaFixing Volunteer)