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

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

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) ... 

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

这是一个基于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); } } 

我们还在开发期间利用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(); } } 

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

不幸的是,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 dispatcherTypes = getSecurityDispatcherTypes(); registration.addMappingForUrlPatterns(dispatcherTypes, !insertBeforeOtherFilters, "/*"); } 

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

 >>>>>> Registering filter 'springSecurityFilterChain' with: class org.springframework.web.filter.DelegatingFilterProxy >>>>>> Existing filter 'springSecurityFilterChain' as: org.springframework.security.web.FilterChainProxy 

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

 >>>>>> Registering filter 'springSecurityFilterChain' with: class org.springframework.web.filter.DelegatingFilterProxy . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v1.2.0.RELEASE) 

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

我认为你的应用程序中必须有一个AbstractSecurityWebApplicationInitializer的具体子类。 Spring的Servlet 3.0支持将找到这个WebApplicationInitializer实现,并在Tomcat启动你的应用程序时调用它。 这会触发尝试注册Spring Security的filter。 您还有扩展SpringBootServletInitializer WebXml类。 这也是一个WebApplicationInitializer ,它将在Tomcat启动你的应用程序时被调用。 由于Spring Boot的自动配置支持,这也会触发尝试注册Spring Security的filter。

您的WebXml类不声明订单(它没有实现Spring的Ordered接口,也没有使用@Order注释)。 我猜你的AbstractSecurityWebApplicationInitializer子类也是如此。 这意味着它们都具有相同的顺序(默认值),因此Spring可以按任意顺序自由调用它们。 当您的AbstractSecurityWebApplicationInitializer子类首先执行时,您的应用程序可以正常工作,因为Spring Boot可以容忍已经存在的filter。 如果Spring Boot首先出现失败,因为AbstractSecurityWebApplicationInitializer不是那么宽容。

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

在Spring引导文档之后,您应该通过在应用程序配置中添加注释@EnableWebMvcSecurity来禁用Spring引导加载的默认安全配置(请参阅75.2更改AuthenticationManager并添加用户帐户 ),而不应该像这样配置Web安全适配器:

 @Bean WebSecurityConfigurerAdapter webSecurityAdapter() { WebSecurityConfigurerAdapter adapter = new WebSecurityConfigurerAdapter() { @Override protected void configure(HttpSecurity http) throws Exception { http....