Summary

I posted this question on StackOverflow : https://stackoverflow.com/questions/71871306/spring-security-how-to-mix-xml-securityhttp-and-java-securityfilterchain-de

When mixing xml configuration and Java configuration, if a <security:http> element exists in the xml configuration file the SecurityFilterChain created in the Java configuration is not used. The method creating the bean is called but it is not present in the FilterChainProxy when requests are processed.

Actual Behavior

By using this sample it is possible to reproduce the issue : https://github.com/KWSimon/minoshill/tree/main/SpringSecurityIssue

  • Run the application : mvn clean package tomee:run

This resource shouldn't be reachable because it is protected by the SecurityFilterChain declared in the testapp.SecurityConfiguration class : http://localhost:8180/testapp/secured/secured.html

  • Stop the application
  • Comment or remove the following element from the SpringConfig.xml file : <security:http pattern="/page/**" security="none"/>

  • Run the application : mvn clean package tomee:run

The following resource is now protected by the filter : http://localhost:8180/testapp/secured/secured.html

Expected Behavior

Both xml and Java configuration should allow to declare and add filters to the SecurityFilterChain.

Configuration

Version

Spring Security 5.6.2

Sample

By using this sample it is possible to reproduce the issue : https://github.com/KWSimon/minoshill/tree/main/SpringSecurityIssue

Comment From: KWSimon

Any idea if it is a configuration issue, an incorrect use of Spring Security or a bug ?

Comment From: marcusdacoregio

Hi @KWSimon, sorry for the delay.

I have reached out to the team to see if that setup is possible, and, if so, how to make it work. I'll get back to you once I have a clear answer for that.

Comment From: rwinch

Mixing <http> and @EnableWebSecurity is not intended to work together. The problem that is happening is that the XML configuration and Java Configuration are both creating a bean by the name of springSecurityConfiguration. The second bean (XML Configuration) overrides the first bean (Java configuration) which is why it is not working.

Can I ask why you want to use XML and Java Configuration together?

Comment From: KWSimon

Hi @rwinch , Thank you for this explanation. To give you some context, the component I'm working on is a library used by most of the Java applications developed by my company. I'd like to move smoothly from xml based configuration to Java based configuration to limit impacts on depending applications. Our xml configuration already contains multiple http elements, most of them are out of the scope of my current task and will be left untouched. It left me with two choices : stick with the xml configuration only, or find a way to have both configuration styles working together.

Comment From: KWSimon

While waiting for a clean solution I imagined this workaround : https://stackoverflow.com/a/71924315/14838319. But as a said in my answer, it uses reflection and therefore could only be a very short term solution :

/**
 * 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);

    }
}

Comment From: rwinch

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>

I created a PR for your sample repository that demonstrates the fixes. The PR also adds some tests so that you can test without starting a container. See the commit history for comments as well. https://github.com/KWSimon/minoshill/pull/1

Comment From: KWSimon

Thanks a lot @rwinch for the explanation, the suggested solution and the sample. I might not use it immediately to avoid breaking changes since I'm working on a library and doesn't have control on the web.xml file of the applications. But I'll do it as soon as I see an opportunity.