显然Spring Boot竞争条件导致重复的springSecurityFilterChain注册

Jam*_*lik 7 java spring spring-security spring-boot

我有一个REST-full Web服务,使用Spring Boot 1.2.0-RELEASE实现,偶尔会在启动时抛出以下异常.

03-Feb-2015 11:42:23.697 SEVERE [localhost-startStop-1] org.apache.catalina.core.ContainerBase.addChildInternal ContainerBase.addChild: start: 
 org.apache.catalina.LifecycleException: Failed to start component [StandardEngine[Catalina].StandardHost[localhost].StandardContext[]]
        at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:154)
        at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:725)
...
Caused by: java.lang.IllegalStateException: Duplicate Filter registration for 'springSecurityFilterChain'. Check to ensure the Filter is only configured once.
        at org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer.registerFilter(AbstractSecurityWebApplicationInitializer.java:215)
        at org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer.insertSpringSecurityFilterChain(AbstractSecurityWebApplicationInitializer.java:147)
...
Run Code Online (Sandbox Code Playgroud)

当我说"偶尔"时,我的意思是简单地重新启动Tomcat服务器(版本8.0.17)将产生此异常或将成功加载而不会出现问题.

这是一个基于Spring Boot构建的Servlet 3.0应用程序,因此我们没有传统的web.xml文件.相反,我们使用Java初始化我们的servlet.

package com.v.dw.webservice;

import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;

public class WebXml extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(ApplicationConfig.class);
    }
}
Run Code Online (Sandbox Code Playgroud)

我们还在mvn spring-boot:run开发期间利用该命令,并且在以这种方式运行时尚未出现此竞争条件.我们的配置的"根"和maven使用的主要方法在同一个类中:

package com.v.dw.webservice;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.actuate.autoconfigure.ManagementSecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;

@SpringBootApplication
@EnableAutoConfiguration(exclude = {ManagementSecurityAutoConfiguration.class, SecurityAutoConfiguration.class})
public class ApplicationConfig {

    public static void main(String[] args) {
        SpringApplication.run(ApplicationConfig.class, args);
    }

    @Value("${info.build.version}")
    private String apiVersion;

    @Bean
    @Primary
    @ConfigurationProperties(prefix="datasource.primary")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }

}
Run Code Online (Sandbox Code Playgroud)

我已经尝试简化我们的身份验证逻辑,以使用自定义内存中身份验证提供程序进行测试.据我所知,这是类路径上唯一的自定义身份验证提供程序,我们不会在应用程序根包之外导入任何配置类.

不幸的是,Spring和Tomcat提供的日志记录输出无法提供有关错误的任何上下文,因此我尝试从此处获取AbstractSecurityWebApplictionInitializer源:

https://raw.githubusercontent.com/spring-projects/spring-security/rb3.2.5.RELEASE/web/src/main/java/org/springframework/security/web/context/AbstractSecurityWebApplicationInitializer.java

我修改了该registerFilter(...)方法,试图通过添加System.out调用来生成一些有用的调试输出.

private final void registerFilter(ServletContext servletContext, boolean insertBeforeOtherFilters, String filterName, Filter filter) {
    System.out.println(">>>>>> Registering filter '" + filterName + "' with: " + filter.getClass().toString());
    Dynamic registration = servletContext.addFilter(filterName, filter);
    if(registration == null) {
        System.out.println(">>>>>> Existing filter '" + filterName + "' as: " + servletContext.getFilterRegistration(filterName).getClassName());
        throw new IllegalStateException("Duplicate Filter registration for '" + filterName +"'. Check to ensure the Filter is only configured once.");
    }
    registration.setAsyncSupported(isAsyncSecuritySupported());
    EnumSet<DispatcherType> dispatcherTypes = getSecurityDispatcherTypes();
    registration.addMappingForUrlPatterns(dispatcherTypes, !insertBeforeOtherFilters, "/*");
}
Run Code Online (Sandbox Code Playgroud)

当它失败时,调试输出仅在异常之前生成一次.这表明该registerFilter(...)方法只在Spring加载过程中调用一次并且相对较晚:

>>>>>> Registering filter 'springSecurityFilterChain' with: class org.springframework.web.filter.DelegatingFilterProxy
>>>>>> Existing filter 'springSecurityFilterChain' as: org.springframework.security.web.FilterChainProxy
Run Code Online (Sandbox Code Playgroud)

当它工作时,调试输出如下所示:

>>>>>> Registering filter 'springSecurityFilterChain' with: class org.springframework.web.filter.DelegatingFilterProxy

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.2.0.RELEASE)
Run Code Online (Sandbox Code Playgroud)

这表明安全配置在加载过程中更早发生,当它失败时.

And*_*son 16

我认为你AbstractSecurityWebApplicationInitializer的应用程序中必须有一个具体的子类.Spring的Servlet 3.0支持将找到这个WebApplicationInitializer实现,并在Tomcat启动你的应用程序时调用它.这会触发尝试注册Spring Security的过滤器.你也有你的WebXml课程延伸SpringBootServletInitializer.WebApplicationInitializer当Tomcat启动你的应用程序时,这也将被调用.由于Spring Boot的自动配置支持,这也会触发尝试注册Spring Security的过滤器.

你的WebXml类没有声明一个命令(它没有实现Spring的Ordered接口,也没有注释@Order).我猜你的AbstractSecurityWebApplicationInitializer子类也是如此.这意味着它们都具有相同的顺序(默认值),因此Spring可以任意顺序调用它们.当您的AbstractSecurityWebApplicationInitializer子类首先运行时,您的应用程序可以正常工作,因为Spring Boot可以容忍已经存在的过滤器.如果在Spring Boot首先出现时失败,那就AbstractSecurityWebApplicationInitializer不那么宽容了.

说完所有这些之后,当你使用Spring Boot时,你可能根本不需要你,AbstractSecurityWebApplicationInitializer所以最简单的解决方案可能是删除它.如果你确实需要它,那么你应该分配它和WebXml一个命令(使用@Order或实现注释Ordered),WebXml以确保始终在AbstractSecurityWebApplicationInitializer子类之后调用它.