全局和局部AuthenticationManager

最近在尝试使用springsecurity新版本做一个授权服务器的功能,在使用过程中结合查资料发现springsecurity的认证管理器有局部和全局的概念,这里通过进行源码的分析,找到相关证明。

Spring Security 5.7 开始WebSecurityConfigurerAdapter 被标记为 @Deprecated (不推荐使用),并在 Spring Security 6 中被移除 推荐我们使用如下方式直接构建过滤器链

less 复制代码
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
        throws Exception {
        
        // 框架会先获取一个新的 HttpSecurity 实例
        // 相当于:http = applicationContext.getBean(HttpSecurity.class);
    http
            .authorizeHttpRequests((authorize) -> authorize
                    .anyRequest().authenticated()
            )
            // Form login handles the redirect to the login page from the
            // authorization server filter chain
            .formLogin(new FormLoginConfigurerCustomer());

    return http.build();
}

1. HttpSecurity http 参数从哪来?

Spring 容器会自动为这个 @Bean 方法注入一个全新的 HttpSecurity 实例

👉 每次创建 SecurityFilterChain Bean 时,都会传入一个"干净的、新创建的" HttpSecurity 实例。

之所以每次都是一个全新的实例 是因为HttpSecurityConfiguration类中有如下配置

scss 复制代码
@Bean(HTTPSECURITY_BEAN_NAME)
@Scope("prototype")
HttpSecurity httpSecurity() throws Exception {
   LazyPasswordEncoder passwordEncoder = new LazyPasswordEncoder(this.context);
   AuthenticationManagerBuilder authenticationBuilder = new DefaultPasswordEncoderAuthenticationManagerBuilder(
         this.objectPostProcessor, passwordEncoder);
   authenticationBuilder.parentAuthenticationManager(authenticationManager());
   authenticationBuilder.authenticationEventPublisher(getAuthenticationEventPublisher());
   HttpSecurity http = new HttpSecurity(this.objectPostProcessor, authenticationBuilder, createSharedObjects());
   WebAsyncManagerIntegrationFilter webAsyncManagerIntegrationFilter = new WebAsyncManagerIntegrationFilter();
   webAsyncManagerIntegrationFilter.setSecurityContextHolderStrategy(this.securityContextHolderStrategy);
   // @formatter:off
   http
      .csrf(withDefaults())
      .addFilter(webAsyncManagerIntegrationFilter)
      .exceptionHandling(withDefaults())
      .headers(withDefaults())
      .sessionManagement(withDefaults())
      .securityContext(withDefaults())
      .requestCache(withDefaults())
      .anonymous(withDefaults())
      .servletApi(withDefaults())
      .apply(new DefaultLoginPageConfigurer<>());
   http.logout(withDefaults());
   // @formatter:on
   applyCorsIfAvailable(http);
   applyDefaultConfigurers(http);
   return http;
}

@Scope("prototype") 确保了每次都能是独立的实例

@Scope("prototype") → 每次 getBean("httpSecurity") 都会重新执行 httpSecurity() 方法,创建新对象

HttpSecurity的创建

java 复制代码
 // 1 创建一个AuthenticationManagerBuilder
 AuthenticationManagerBuilder authenticationBuilder = new DefaultPasswordEncoderAuthenticationManagerBuilder(
         this.objectPostProcessor, passwordEncoder);
   // 2设置父级认证管理器      
   authenticationBuilder.parentAuthenticationManager(authenticationManager());
   authenticationBuilder.authenticationEventPublisher(getAuthenticationEventPublisher());
   //3 创建HttpSecurity
   HttpSecurity http = new HttpSecurity(this.objectPostProcessor, authenticationBuilder, createSharedObjects());

构造器如下图:

所以每个httpScurity内部都有一个独立的AuthenticationManagerBuilder 保存在sharedObjects属性中 这个是共享属性,确保后续使用的时候可以从共享属性中再次获取到

AuthenticationManagerBuilder

AuthenticationManagerBuilder是用来构建AuthenticationManager认证管理器的,每个HttpSecurity都持有一个自己的AuthenticationManagerBuilder。

AuthenticationManager是认证管理器,并在身份认证成功后返回一个经过认证的 Authentication 对象。AuthenticationManager 是一个接口,ProviderManager实现了这个接口

ProviderManager 维护了一个 AuthenticationProvider 列表,每个 AuthenticationProvider 都是一个认证器,不同的 AuthenticationProvider 用来处理不同的 Authentication 对象的认证。一次完整的身份认证流程可能会经过多个 AuthenticationProvider。

每一个 ProviderManager 管理多个 AuthenticationProvider,同时每一个 ProviderManager 都可以配置一个 parent,如果当前的 ProviderManager 中认证失败了,还可以去它的 parent 中继续执行认证

根据配置方式和作用范围,我们可以将 AuthenticationManager 分为:

  • "全局" :指被注册为 Spring Bean 的、可被整个应用共享的 AuthenticationManager
  • "局部" :指仅在某个 SecurityFilterChain 内部创建并使用的 AuthenticationManager,作用范围有限

全局认证管理器

scss 复制代码
DefaultPasswordEncoderAuthenticationManagerBuilder(
         this.objectPostProcessor, passwordEncoder);
   authenticationBuilder.parentAuthenticationManager(authenticationManager());

前面我们了解到构建httpsecurity的时候需要创建authenticationBuilder,authenticationBuilder会通过调用authenticationManager()方法来设置全局认证管理器

csharp 复制代码
private AuthenticationManager authenticationManager() throws Exception {
   return this.authenticationConfiguration.getAuthenticationManager();
}

底层调用的是AuthenticationConfiguration中的方法

kotlin 复制代码
public AuthenticationManager getAuthenticationManager() throws Exception {
   if (this.authenticationManagerInitialized) {
      return this.authenticationManager;
   }
   AuthenticationManagerBuilder authBuilder = this.applicationContext.getBean(AuthenticationManagerBuilder.class);
   if (this.buildingAuthenticationManager.getAndSet(true)) {
      return new AuthenticationManagerDelegator(authBuilder);
   }
   for (GlobalAuthenticationConfigurerAdapter config : this.globalAuthConfigurers) {
      authBuilder.apply(config);
   }
   this.authenticationManager = authBuilder.build();
   if (this.authenticationManager == null) {
      this.authenticationManager = getAuthenticationManagerBean();
   }
   this.authenticationManagerInitialized = true;
   return this.authenticationManager;
}

上面这段方法确保了全局认证管理器只能被初始化一次,调用多次返回的是已经构建好的,基本原理还是从容器中获取到AuthenticationManagerBuilder,调用这个builder的build方法来构建。下面的代码展示了builder是如何放进容器的

java 复制代码
@Configuration(proxyBeanMethods = false)
@Import(ObjectPostProcessorConfiguration.class)
public class AuthenticationConfiguration {

   
   // 在容器中放了一个AuthenticationManagerBuilder 将来用来构建全局认证管理器
   @Bean
   public AuthenticationManagerBuilder authenticationManagerBuilder(ObjectPostProcessor<Object> objectPostProcessor,
         ApplicationContext context) {
      LazyPasswordEncoder defaultPasswordEncoder = new LazyPasswordEncoder(context);
      AuthenticationEventPublisher authenticationEventPublisher = getAuthenticationEventPublisher(context);
      DefaultPasswordEncoderAuthenticationManagerBuilder result = new DefaultPasswordEncoderAuthenticationManagerBuilder(
            objectPostProcessor, defaultPasswordEncoder);
      if (authenticationEventPublisher != null) {
         result.authenticationEventPublisher(authenticationEventPublisher);
      }
      return result;
   }

局部认证管理器

根据前面的介绍,HttpSecurity 在构建的时候就会传入 AuthenticationManagerBuilder,传入进来的 AuthenticationManagerBuilder ,被保存到SharedObject 里边去了缓存了起来

最终httpsecurity在build的过程中 会先执行beforeConfigure方法,用来构建局部认证管理器

scss 复制代码
@Override
protected void beforeConfigure() throws Exception {
   if (this.authenticationManager != null) {
      setSharedObject(AuthenticationManager.class, this.authenticationManager);
   }
   else {
      ObservationRegistry registry = getObservationRegistry();
      AuthenticationManager manager = getAuthenticationRegistry().build();
      if (!registry.isNoop() && manager != null) {
         setSharedObject(AuthenticationManager.class, new ObservationAuthenticationManager(registry, manager));
      }
      else {
         setSharedObject(AuthenticationManager.class, manager);
      }
   }
}

你会发现getAuthenticationRegistry中返回的就是提前缓存好的authenticationManagerBuilder,最终构建出来的认证管理器是局部管理器

kotlin 复制代码
@Override
public HttpSecurity authenticationProvider(AuthenticationProvider authenticationProvider) {
   getAuthenticationRegistry().authenticationProvider(authenticationProvider);
   return this;
}

@Override
public HttpSecurity userDetailsService(UserDetailsService userDetailsService) throws Exception {
   getAuthenticationRegistry().userDetailsService(userDetailsService);
   return this;
}

private AuthenticationManagerBuilder getAuthenticationRegistry() {
   return getSharedObject(AuthenticationManagerBuilder.class);
}

总结

总结下 本文主要讲解了 1.httpsecurity采用了多实例的方式,作者这么设计的原因是因为一个应用可以配置多条过滤器链,每个过滤器链需要单独的进行配置,httpSecurity就是过滤器链的配置类,所以需要多实例。

2.有全局认证管理器和局部认证管理器,全局认证管理器使用容器中的builder进行构建的,整个应用共享 局部认证管理器是和每个过滤器链,也就是每个HttpSecurity实例绑定的。

相关推荐
欧哈东哥9 分钟前
【C++】标准库中用于组合多个值的数据结构pair、tuple、array...
java·数据结构·c++
python_13613 分钟前
web请求和响应
java·spring·github
柏油31 分钟前
可视化 MySQL binlog 监听方案
数据库·后端·mysql
舒一笑44 分钟前
Started TttttApplication in 0.257 seconds (没有 Web 依赖导致 JVM 正常退出)
jvm·spring boot·后端
M1A11 小时前
Java Enum 类:优雅的常量定义与管理方式(深度解析)
后端
AAA修煤气灶刘哥2 小时前
别再懵了!Spring、Spring Boot、Spring MVC 的区别,一篇讲透
后端·面试
ciku2 小时前
Spring AI Starter和文档解读
java·人工智能·spring
柏油2 小时前
MySQL 字符集 utf8 与 utf8mb4
数据库·后端·mysql