有的時候鬆哥會和大家分享一些 Spring Security 的冷門用法,不是為了顯擺,只是希望大家能夠從不同的角度加深對 Spring Security 的理解,這些冷門的用法非常有助於大家理解 Spring Security 的內部工作原理。我本來可以純粹的去講原始碼,講原理,但是那樣太枯燥了,所以我會盡量通過一些小的案例來幫助大家理解原始碼,這些案例的目的只是為了幫助大家理解 Spring Security 原始碼,僅此而已!所以請大家不要和我抬槓這些使用者定義方式沒用!
好啦,我今天要給大家表演一個絕活,就是花式定義使用者物件。希望大家通過這幾個案例,能夠更好的理解 ProviderManager 的工作機制。
本文內容和上篇文章【深入理解 AuthenticationManagerBuilder 【原始碼篇】】內容強關聯,所以強烈建議先學習下上篇文章內容,再來看本文,就會好理解很多。
1.絕活一
先來看如下一段程式碼:
@Configuration
public class SecurityConfig {
@Bean
UserDetailsService us() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("sang").password("{noop}123").roles("admin").build());
return manager;
}
@Configuration
@Order(1)
static class DefaultWebSecurityConfig extends WebSecurityConfigurerAdapter {
UserDetailsService us1() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("javaboy").password("{noop}123").roles("admin", "aaa", "bbb").build());
return manager;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/foo/**")
.authorizeRequests()
.anyRequest().hasRole("admin")
.and()
.formLogin()
.loginProcessingUrl("/foo/login")
.permitAll()
.and()
.userDetailsService(us1())
.csrf().disable();
}
}
@Configuration
@Order(2)
static class DefaultWebSecurityConfig2 extends WebSecurityConfigurerAdapter {
UserDetailsService us2() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("江南一點雨").password("{noop}123").roles("user", "aaa", "bbb").build());
return manager;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/bar/**")
.authorizeRequests()
.anyRequest().hasRole("user")
.and()
.formLogin()
.loginProcessingUrl("/bar/login")
.permitAll()
.and()
.csrf().disable()
.userDetailsService(us2());
}
}
}
看過前面文章(Spring Security 竟然可以同時存在多個過濾器鏈?)的小夥伴應該明白,這裡鬆哥定義了兩個過濾器鏈,這個相信大家都能理解,不理解的話,參考Spring Security 竟然可以同時存在多個過濾器鏈?一文。
但是大家注意,在每一個過濾器鏈中,我都提供了一個 UserDetailsService 例項,然後在 configure(HttpSecurity http) 方法中,配置這個 UserDetailsService 例項。除了每一個過濾器鏈中都配置一個 UserDetailsService 之外,我還提供了一個 UserDetailsService 的 Bean,所以這裡前前後後相當於一共有三個使用者,那麼我們登入時候,使用哪個使用者可以登入成功呢?
先說結論:
- 如果登入地址是 /foo/login,那麼通過 sang 和 javaboy 兩個使用者可以登入成功。
- 如果登入地址是 /bar/login,那麼通過 sang 和 江南一點雨 兩個使用者可以登入成功。
也就是說,那個全域性的,公共的 UserDetailsService 總是有效的,而針對不同過濾器鏈配置的 UserDetailsService 則只針對當前過濾器鏈生效。
鬆哥這裡為了方便,使用了基於記憶體的 UserDetailsService,當然你也可以替換為基於資料庫的 UserDetailsService。
那麼接下來我們就來分析一下,為什麼是這個樣子?
1.1 原始碼分析
1.1.1 全域性 AuthenticationManager
首先大家注意,雖然我定義了兩個過濾器鏈,但是在兩個過濾器鏈的定義中,我都沒有重寫 configure(AuthenticationManagerBuilder auth) 方法,結合上篇文章,沒有重寫這個方法,就意味著 AuthenticationConfiguration 中提供的全域性 AuthenticationManager 是有效的,也就是說,系統預設提供的 AuthenticationManager 將作為其他區域性 AuthenticationManager 的 parent。
那麼我們來看下全域性的 AuthenticationManager 配置都配了啥?
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 : globalAuthConfigurers) {
authBuilder.apply(config);
}
authenticationManager = authBuilder.build();
if (authenticationManager == null) {
authenticationManager = getAuthenticationManagerBean();
}
this.authenticationManagerInitialized = true;
return authenticationManager;
}
全域性的配置中,有一步就是遍歷 globalAuthConfigurers,遍歷全域性的 xxxConfigurer,並進行配置。全域性的 xxxConfigurer 一共有三個,分別是:
- EnableGlobalAuthenticationAutowiredConfigurer
- InitializeUserDetailsBeanManagerConfigurer
- InitializeAuthenticationProviderBeanManagerConfigurer
其中 InitializeUserDetailsBeanManagerConfigurer,看名字就是用來配置 UserDetailsService 的,我們來看下:
@Order(InitializeUserDetailsBeanManagerConfigurer.DEFAULT_ORDER)
class InitializeUserDetailsBeanManagerConfigurer
extends GlobalAuthenticationConfigurerAdapter {
@Override
public void init(AuthenticationManagerBuilder auth) throws Exception {
auth.apply(new InitializeUserDetailsManagerConfigurer());
}
class InitializeUserDetailsManagerConfigurer
extends GlobalAuthenticationConfigurerAdapter {
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
if (auth.isConfigured()) {
return;
}
UserDetailsService userDetailsService = getBeanOrNull(
UserDetailsService.class);
if (userDetailsService == null) {
return;
}
PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);
UserDetailsPasswordService passwordManager = getBeanOrNull(UserDetailsPasswordService.class);
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
if (passwordEncoder != null) {
provider.setPasswordEncoder(passwordEncoder);
}
if (passwordManager != null) {
provider.setUserDetailsPasswordService(passwordManager);
}
provider.afterPropertiesSet();
auth.authenticationProvider(provider);
}
}
}
可以看到,InitializeUserDetailsBeanManagerConfigurer 中定義了內部類,在其內部類的 configure 方法中,通過 getBeanOrNull 去從容器中查詢 UserDetailsService 例項,查詢到之後,建立 DaoAuthenticationProvider,並最終配置給 auth 物件。
這裡的 getBeanOrNull 方法從容器中查詢到的,實際上就是 Spring 容器中的 Bean,也就是我們一開始配置了 sang 使用者的那個 Bean,這個 Bean 被交給了全域性的 AuthenticationManager,也就是所有區域性 AuthenticationManager 的 parent。
1.1.2 區域性 AuthenticationManager
通過上篇文章的學習,小夥伴們知道了所有 HttpSecurity 在構建的過程中,都會傳遞一個區域性的 AuthenticationManagerBuilder 進來,這個區域性的 AuthenticationManagerBuilder 一旦傳進來就存入了共享物件中,以後需要用的時候再從共享物件中取出來,部分程式碼如下所示:
public HttpSecurity(ObjectPostProcessor<Object> objectPostProcessor,
AuthenticationManagerBuilder authenticationBuilder,
Map<Class<?>, Object> sharedObjects) {
super(objectPostProcessor);
Assert.notNull(authenticationBuilder, "authenticationBuilder cannot be null");
setSharedObject(AuthenticationManagerBuilder.class, authenticationBuilder);
//省略
}
private AuthenticationManagerBuilder getAuthenticationRegistry() {
return getSharedObject(AuthenticationManagerBuilder.class);
}
所以,我們在 HttpSecurity 中配置 UserDetailsService,實際上是給這個 AuthenticationManagerBuilder 配置的:
public HttpSecurity userDetailsService(UserDetailsService userDetailsService)
throws Exception {
getAuthenticationRegistry().userDetailsService(userDetailsService);
return this;
}
也就是區域性 AuthenticationManager。
至此,整個流程就很清晰了。
鬆哥再結合下面這張圖給大家解釋下:
每一個過濾器鏈都會繫結一個自己的 ProviderManager(即 AuthenticationManager 的實現),而每一個 ProviderManager 中都通過 DaoAuthenticationProvider 持有一個 UserDetailsService 物件,你可以簡單理解為一個 ProviderManager 管理了一個 UserDetailsService,當我們開始認證的時候,首先由過濾器鏈所持有的區域性 ProviderManager 去認證,要是認證失敗了,則呼叫 ProviderManager 的 parent 再去認證,此時就會用到全域性 AuthenticationManager 所持有的 UserDetailsService 物件了。
結合一開始的案例,例如你的登入地址是 /foo/login
,如果你的登入使用者是 sang/123,那麼先去 HttpSecurity 的區域性 ProviderManager 中去驗證,結果驗證失敗(區域性的 ProviderManager 中對應的使用者是 javaboy),此時就會進入區域性 ProviderManager 的 parent 中去認證,也就是全域性認證,全域性的 ProviderManager 中對應的使用者就是 sang 了,此時就認證成功。
可能有點繞,這個過程大家結合上篇文章仔細品一品。
2.絕活二
再次修改 SecurityConfig 的定義,如下:
@Configuration
public class SecurityConfig {
@Bean
UserDetailsService us() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("sang").password("{noop}123").roles("admin").build());
return manager;
}
@Configuration
@Order(1)
static class DefaultWebSecurityConfig extends WebSecurityConfigurerAdapter {
UserDetailsService us1() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("javaboy").password("{noop}123").roles("admin", "aaa", "bbb").build());
return manager;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(us1());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/foo/**")
.authorizeRequests()
.anyRequest().hasRole("admin")
.and()
.formLogin()
.loginProcessingUrl("/foo/login")
.permitAll()
.and()
.csrf().disable();
}
}
@Configuration
@Order(2)
static class DefaultWebSecurityConfig2 extends WebSecurityConfigurerAdapter {
UserDetailsService us2() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("江南一點雨").password("{noop}123").roles("user", "aaa", "bbb").build());
return manager;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(us2());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/bar/**")
.authorizeRequests()
.anyRequest().hasRole("user")
.and()
.formLogin()
.loginProcessingUrl("/bar/login")
.permitAll()
.and()
.csrf().disable();
}
}
}
和前面相比,這段程式碼的核心變化,就是我重寫了 configure(AuthenticationManagerBuilder auth)
方法,根據上篇文章的介紹,重寫了該方法之後,全域性的 AuthenticationMananger 定義就失效了,也就意味著 sang 這個使用者定義失效了,換言之,無論是 /foo/login
還是 /bar/login
,使用 sang/123 現在都無法登入了。
在每一個 HttpSecurity 過濾器鏈中,我都重寫了 configure(AuthenticationManagerBuilder auth)
方法,並且重新配置了 UserDetailsService,這個重寫,相當於我在定義 parent 級別的 ProviderManager。而每一個 HttpSecurity 過濾器鏈則不再包含 UserDetailsService。
當使用者登入時,先去找到 HttpSecurity 過濾器鏈中的 ProviderManager 去認證,結果認證失敗,然後再找到 ProviderManager 的 parent 去認證,就成功了。
3.小結
在實際開發中,這樣配置你幾乎不會見到,但是上面兩個案例,可以讓你更好的理解 Spring Security 的認證過程,小夥伴們可以仔細品一品~
好啦,本文就先說這麼多,案例下載地址https://github.com/lenve/spring-security-samples
如果小夥伴們覺得有收穫,記得點個在看鼓勵下鬆哥哦~