Spring Security的认证配置
本来是想把Spring Security中用户管理由内存改到数据库,但没想到兜兜转转,还挺复杂。
这里面设计到建造器,配置器,过滤器,还有父子认证提供器。
WebSecurity是作为一个总领的建造者,作用是将各个HttpSecurity建立的过滤器链再构建成一个更大的过滤器链。
HttpSecurity作为一个建造者,会先收集各种配置器到自身,在建造过程中各个适配器会释放出一个个过滤器,最后会将所有的过滤器建成一个过滤器链返回为WebSecurity。
此外还有另一个建造这,那就是今天要说的的认证管理建造者AuthenticationManagerBuilder,它的作用并不是去建造一个过滤器或过滤器链,而只是建造一个AuthenticationManager,并在HttpSecurity中已AuthenticationManager.class为键名进行保存。
由此处就可看出,如果容器中存在自定义的UserDetailsService,则会自动创建一个应用此UserDetailsService的DaoAuthenticationProvider。对于PasswordEncoder和UserDetailsPasswordService,如果容器中存在也会一并去除并应用。
这里要求容器中必须有且只有一个对象,才会自动创建,以防止歧义。
至此在获取HttpSecurity时通过全局的认证配置器配置并建造了HttpSecurity所需的AuthenticationManager。
对于获取的AuthenticationManager,HttpSecurity并没有直接拿来用,而是作为作为自己直接AuthenticationManager的父认证管理器。
在通过注入初始化WebSecurityConfigurerAdapter时,会将其直属认证管理器初始化为DefaultPasswordEncoderAuthenticationManagerBuilder,当然只是个建造器而已。
至此HttpSecurity就有了直属的认证管理建造器,并且该直属建造器还有了父认证管理器。此时就只等HttpSecurity执行建造过程。
但是建造过程主要是执行各个配置器的过程,配置器生成并释放过滤器到HttpSecurity,HttpSecurity在将这个过滤器整合成一个大的过滤器链。
这些过滤器里就有一个执行认证的过滤器UsernamePasswordAuthenticationFilter。
那这个过滤器是从哪来的呢?
就是通过配置器FormLoginConfigurer声明,并通过其父类AbstractAuthenticationFilterConfigurer在对HttpSecurity进行配置时将其注入的。
在注入的时候还会通过过滤器的方法setAuthenticationManager(),将HttpSecurity中的已存在的认证管理器再注入到过滤器中。
这样认证过滤器在进行认证的时候就可以使用HttpSecurity初始化时建立的直属认证管理器。
此时就涉及到一个先后问题,配置器FormLoginConfigurer在向HttpSecurity释放认证过滤器时,会将HttpSecurity中的认证管理器先注入到过滤器中。这就要求在注入过滤器之前,认证管理器要已经存在了。根据前述我们可以知道,在初始化HttpSecurity时只是保存了一个认证管理器的建造器,并没与执行真正的建造过程。
所以执行建造是一定的,但就看何时建造了。根据前述分析,这个建造过程一定是早于认证配置器对HttpSecurity进行配置之前。
在HttpSecurity的父类AbstractConfiguredSecurityBuilder的构造过程中,早于配置的步骤就三个beforeInit(),init(),beforeConfigure()。
再去查看HttpSecurity,果然在方法beforeConfigure()中,先根据AuthenticationManagerBuilder.class获取认证管理建造器,再执行建造过程生成真正的管理器,并以AuthenticationManager.class作为键名存入HttpSecurity中。
这样在后续配置认证过滤器时就可以从HttpSecurity中获取并注入。
AuthenticationManagerBuilder建造出的是借口AuthenticationManager的实现类ProviderManager。
认证的过程是通过ProviderManager自身的AuthenticationProvider集合和父认证管理器AuthenticationManager组合完成。当其自身自身的AuthenticationProvider集合无法完成验证时,就需要由父认证管理器AuthenticationManager接手继续进行验证。但只要有一个子认证提供者AuthenticationProvider能够验证通过并返回结果,则认证就结束了。
在AuthenticationManagerBuilder初始构建认证管理器ProviderManager时,其子认证提供者集合AuthenticationProvider是空的,所以也就只能通过在HttpSecurity初始化时传入的父认证管理器来验证。
而父认证管理器建造器被初始化DefaultPasswordEncoderAuthenticationManagerBuilder,并且没有再为其指定父管理器,所以HttpSecurity的直属认证管理器也就没有祖父管理器。
虽然HttpSecurity的直属管理器构造器和父管理器构造器的类名相同,都是DefaultPasswordEncoderAuthenticationManagerBuilder,但它们却分别为不同类的内部类,直属管理器的是WebSecurityConfigurerAdapter下的内部类,父认证管理器的是AuthenticationConfiguration下的内部类。
由于父认证管理器的建造器应用了全局认证配置器,所以当我们将自定义的UserDetailsService注解为Bean并被Spring收录到对象容器中时,就会自动生成一个默认的认证提供者DaoAuthenticationProvider并注入建造器的认证提供者集合。
在后续执行建造父认证管理器时就会获取到并将其注入,然后就会应用到登录过滤器对登录请求的认证过程中。
在调试时发现在HttpSecurity的直属认证管理里中总会存在一个匿名认证提供者AnonymousAuthenticationProvider,按理说认证提供者集合providers应该为空才对,所有登录请求都要转到父认证管理器处理。
这是为什么呢?
这就涉及到HttpSecurity的默认初始化了。
默认初始化时会通方法anonymous()应用一个配置器AnonymousConfigurer。
该匿名配置器在初始化时会生成一个AnonymousAuthenticationProvider,并加入到HttpSecurity直属的认证管理器的建造器中。注意是建造器AuthenticationManagerBuilder中,而不是HttpSecurity的直属管理器AuthenticationManager中 ,因为此时只是配置阶段即只是收集注入各种配置器,还未执行任何建造操作,也即HttpSecurity中不存在直属的实例化认证管理器AuthenticationManager。
在HttpSecurity的父类AbstractConfiguredSecurityBuilder建造过程中,初始化阶段调用所有配置器的init()方法。
对于配置来说AnonymousConfigurer就是创建AnonymousAuthenticationProvider,并放入HttpSecurity的直属认证建造器AuthenticationManagerBuilder的认证提供者集合authenticationProviders中。
在beforeConfigure阶段,HttpSecurity通过建造器将直属认证管理器ProviderManager建造出来。
同时也一并将建造器中收集的所有认证提供者注入要生成的认证管理器中,从而形成HttpSecurity的直属认证提供者。
在处理登录请求时就可以依次取出,先判断是否支持,然后再执行验证操作。
学习Spring Security主要看的是松哥的一系列教程。
关于如何将对用户的检索替换为自己的数据库查询,松哥的教程如下。
他的建议是在WebSecurityConfigurerAdapter中通过configure()方法来设置AuthenticationManagerBuilder的userDetailsService。但这种方式会改变HttpSecurity生成父认证管理器的默认逻辑。
最终也会在AuthenticationManagerBuilder的配置阶段,通过配置器将包含指定UserDetailsService的AuthenticationProvider注入到认证管理建造器中,最终会注入HttpSecurity的父认证管理器中。
但默认逻辑是以disableLocalConfigureAuthenticationBldr为true作为标记,通过认证管理器配置类AuthenticationConfiguration从Spring对象容器中动态获取认证提供者AuthenticationProvider所需的组件UserDetailsService、PasswordEncoder、UserDetailsPasswordService。当然这个过程也是通过配置器实现的,只不过是一个全局性质的内部类InitializeUserDetailsManagerConfigurer。
如果按照松哥介绍的那样,在WebSecurityConfigurerAdapter中通过configure()去配置AuthenticationManagerBuilder,那就无法利用Spring注解Bean的方式动态灵活的去配置HttpSecurity的父认证管理器了,而是AuthenticationProvider所需的所有组件都只能都configure()中完成配置。