为什么HttpSecurity会初始化创建两次

现象

最近在使用springsecurity,想要做一个授权服务器。在使用过程中,调试断点的时候发现HttpSecurity的初始化会执行两次。这个是不符合预期的。因为我们都知道如果我们引入了springsecurity的依赖后,由于自动配置,springsecurity会自动配置一条默认的过滤器链。

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

以前我们分析过SpringBootWebSecurityConfiguration这个配置类,它会检查如果用户没有主动配置一条SecurityFilterChain,框架会提供一个默认的过滤器链。@ConditionalOnDefaultWebSecurity这个注解是条件注解,它就是来确保如果用户没有自定义过滤器链,那么这个配置会生效。

less 复制代码
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
class SpringBootWebSecurityConfiguration {

   /**
    * The default configuration for web security. It relies on Spring Security's
    * content-negotiation strategy to determine what sort of authentication to use. If
    * the user specifies their own {@link SecurityFilterChain} bean, this will back-off
    * completely and the users should specify all the bits that they want to configure as
    * part of the custom security configuration.
    */
   @Configuration(proxyBeanMethods = false)
   @ConditionalOnDefaultWebSecurity
   static class SecurityFilterChainConfiguration {

      @Bean
      @Order(SecurityProperties.BASIC_AUTH_ORDER)
      SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
         http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
         http.formLogin(withDefaults());
         http.httpBasic(withDefaults());
         return http.build();
      }

   }
  }

如果上面的配置生效,框架在启动的时候就会调用defaultSecurityFilterChain(HttpSecurity http) 方法,并且注入一个干净的HttpSecurity实例。正常情况下springboot的依赖注入是单例模式的,但是HttpSecurity这个类比较特殊。HttpSecurityConfiguration在配置HttpSecurity的时候,将他的作用域scope设置为prototype,如下图所示:

这就意味着每当创建SecurityFilterChain的时候,需要注入HttpSecurity的话,框架就会自动调用HttpSecurity httpSecurity() 方法返回一个全新的HttpSecurity,它不是全局唯一的。

如今通过调试发现HttpSecurity httpSecurity() 方法竟然调用了两次。那么到底还有什么地方需要依赖注入HttpSecurity呢?

WebSecurityConfiguration

我们知道FilterChainProxy是整个springsecurity的入口。那么FilterChainProxy 实例是如何被创建的?其实是由WebSecurityConfiguration配置的。下面的代码摘自 WebSecurityConfiguration。

WebSecurityConfiguration最后在Filter springSecurityFilterChain() 方法中通过this.httpSecurity.build() 构建了一个FilterChainProxy的。

Spring Security 支持添加多个SecurityFilterChain,每个SecurityFilterChain负责不同的请求(比如依据请求地址进行区分),这样可以为不同的请求设置不同的认证规则

具体来说,当请求到达FilterChainProxy时,其内部会根据当前请求匹配得到对应的SecurityFilterChain,然后将请求依次转发给到该SecurityFilterChain中的所有 Security Filters。

kotlin 复制代码
@Autowired(required = false)
private HttpSecurity httpSecurity;

@Autowired(required = false)
void setFilterChains(List<SecurityFilterChain> securityFilterChains) {
   this.securityFilterChains = securityFilterChains;
}

@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain() throws Exception {
   // 首先检查securityFilterChains是否为空
   boolean hasFilterChain = !this.securityFilterChains.isEmpty();
   if (!hasFilterChain) {
      // 提供一条默认的过滤器链进行兜底
      this.webSecurity.addSecurityFilterChainBuilder(() -> {
         this.httpSecurity.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated());
         this.httpSecurity.formLogin(Customizer.withDefaults());
         this.httpSecurity.httpBasic(Customizer.withDefaults());
         return this.httpSecurity.build();
      });
   }
   // 将用户或者框架配置的SecurityFilterChain包装成builder,方便后续统一调用
   for (SecurityFilterChain securityFilterChain : this.securityFilterChains) {
      this.webSecurity.addSecurityFilterChainBuilder(() -> securityFilterChain);
   }
   for (WebSecurityCustomizer customizer : this.webSecurityCustomizers) {
      customizer.customize(this.webSecurity);
   }
   return this.webSecurity.build();
}

上面创建的过程中首先检查securityFilterChains集合是否为空。如果配置了SecurityFilterChain(无论是用户自定义还是框架默认配置的)都会通过Autowired的方式注入到securityFilterChains集合中

ini 复制代码
private List<SecurityFilterChain> securityFilterChains = Collections.emptyList();

因为FilterChainProxy依赖securityFilterChains来处理请求 如果过滤器链为空 那么构建FilterChainProxy就没有任何意义了。所以框架做了一次安全检查,如果为空,那么就执行下面的代码。

kotlin 复制代码
 this.webSecurity.addSecurityFilterChainBuilder(() -> {
         this.httpSecurity.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated());
         this.httpSecurity.formLogin(Customizer.withDefaults());
         this.httpSecurity.httpBasic(Customizer.withDefaults());
         return this.httpSecurity.build();
      });
kotlin 复制代码
public WebSecurity addSecurityFilterChainBuilder(
      SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder) {
   this.securityFilterChainBuilders.add(securityFilterChainBuilder);
   return this;
}

WebSecurity有个securityFilterChainBuilders集合,用来收集所有过滤器链的构建者。将在在执行build()的时候就可以取出这里面提前保存的构建者构建出一条过滤器链。

其实看到这里我们已经找到答案了。框架为我们进行了兜底操作,如果没有过滤器链,那么就通过lambda表达式包装成builder,后续构建出一条过滤器链使用,而构建的方法是this.httpSecurity.build()

所以 这就是第二次httpsecurity初始化的地方。在WebSecurityConfiguration中注入了httpsecurity实例,将来用来build出一条过滤器链。

下面的这段代码摘自WebSecurity,将来调用build方法的时候,最终会调用到performBuild方法中,这个方法我现在不细讲了,后面单独出一个文章专门来讲解WebSecurity构建filterProxy的过程。

kotlin 复制代码
@Override
protected Filter performBuild() throws Exception {
   Assert.state(!this.securityFilterChainBuilders.isEmpty(),
         () -> "At least one SecurityBuilder<? extends SecurityFilterChain> needs to be specified. "
               + "Typically this is done by exposing a SecurityFilterChain bean. "
               + "More advanced users can invoke " + WebSecurity.class.getSimpleName()
               + ".addSecurityFilterChainBuilder directly");
   int chainSize = this.ignoredRequests.size() + this.securityFilterChainBuilders.size();
   List<SecurityFilterChain> securityFilterChains = new ArrayList<>(chainSize);
   List<RequestMatcherEntry<List<WebInvocationPrivilegeEvaluator>>> requestMatcherPrivilegeEvaluatorsEntries = new ArrayList<>();
   for (RequestMatcher ignoredRequest : this.ignoredRequests) {
      WebSecurity.this.logger.warn("You are asking Spring Security to ignore " + ignoredRequest
            + ". This is not recommended -- please use permitAll via HttpSecurity#authorizeHttpRequests instead.");
      SecurityFilterChain securityFilterChain = new DefaultSecurityFilterChain(ignoredRequest);
      securityFilterChains.add(securityFilterChain);
      requestMatcherPrivilegeEvaluatorsEntries
         .add(getRequestMatcherPrivilegeEvaluatorsEntry(securityFilterChain));
   }
   for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : this.securityFilterChainBuilders) {
      SecurityFilterChain securityFilterChain = securityFilterChainBuilder.build();
      securityFilterChains.add(securityFilterChain);
      requestMatcherPrivilegeEvaluatorsEntries
         .add(getRequestMatcherPrivilegeEvaluatorsEntry(securityFilterChain));
   }
   if (this.privilegeEvaluator == null) {
      this.privilegeEvaluator = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator(
            requestMatcherPrivilegeEvaluatorsEntries);
   }
   FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
   if (this.httpFirewall != null) {
      filterChainProxy.setFirewall(this.httpFirewall);
   }
   if (this.requestRejectedHandler != null) {
      filterChainProxy.setRequestRejectedHandler(this.requestRejectedHandler);
   }
   else if (!this.observationRegistry.isNoop()) {
      CompositeRequestRejectedHandler requestRejectedHandler = new CompositeRequestRejectedHandler(
            new ObservationMarkingRequestRejectedHandler(this.observationRegistry),
            new HttpStatusRequestRejectedHandler());
      filterChainProxy.setRequestRejectedHandler(requestRejectedHandler);
   }
   filterChainProxy.setFilterChainDecorator(getFilterChainDecorator());
   filterChainProxy.afterPropertiesSet();

   Filter result = filterChainProxy;
   if (this.debugEnabled) {
      this.logger.warn("\n\n" + "********************************************************************\n"
            + "**********        Security debugging is enabled.       *************\n"
            + "**********    This may include sensitive information.  *************\n"
            + "**********      Do not use in a production system!     *************\n"
            + "********************************************************************\n\n");
      result = new DebugFilter(filterChainProxy);
   }

   this.postBuildAction.run();
   return result;
}

但是里面有个代码我想提一下 就是在构建过程中最终会遍历securityFilterChainBuilders所有的securityFilterChainBuilder,然后调用securityFilterChainBuilder的build()方法返回一条过滤器链。这和我上面提到的正好相呼应。通过包装成builder的方式,统一收集在securityFilterChainBuilders集合中,这里统一调用。

ini 复制代码
for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : this.securityFilterChainBuilders) {
   SecurityFilterChain securityFilterChain = securityFilterChainBuilder.build();
   securityFilterChains.add(securityFilterChain);
   requestMatcherPrivilegeEvaluatorsEntries
      .add(getRequestMatcherPrivilegeEvaluatorsEntry(securityFilterChain));
}

总结

终于找到HttpSecurity会被创建两次的关键原因了

1:构建SecurityFilterChain的时候需要注入HttpSecurity,框架一旦发现需要注入HttpSecurity,就会调用@bean标注的HttpSecurity httpSecurity()方法创建一个新实例 因为它是prototype类型的

2:WebSecurityConfiguration类为我们进行了兜底操作,防止我们不配置过滤器链。也注入了HttpSecurity实例,用于将来帮助开发者创建过滤器链

之所以执着的想要知道原因,是因为我讨厌那种黑盒子的感觉,不知道底层发生了什么,会感觉到恐慌。我将整个过程记录下来,一是如果大家恰好有这个疑虑那么正好这个文章会对您有帮助。而是我也是希望我自己真正明白了。

相关推荐
杨DaB12 分钟前
【SpringBoot】Swagger 接口工具
java·spring boot·后端·restful·swagger
YA33313 分钟前
java基础(九)sql基础及索引
java·开发语言·sql
why技术25 分钟前
也是震惊到我了!家里有密码锁的注意了,这真不是 BUG,是 feature。
后端·面试
桦说编程33 分钟前
方法一定要有返回值 \ o /
java·后端·函数式编程
小李是个程序1 小时前
登录与登录校验:Web安全核心解析
java·spring·web安全·jwt·cookie
David爱编程1 小时前
Java 创建线程的4种姿势,哪种才是企业级项目的最佳实践?
java·后端
hrrrrb2 小时前
【Java Web 快速入门】十一、Spring Boot 原理
java·前端·spring boot
Java微观世界2 小时前
Object核心类深度剖析
java·后端
hinotoyk2 小时前
TimeUnit源码分享
java