花式玩 Spring Security ,這樣的用戶定義方式你可能沒見過!

有的時候鬆哥會和你們分享一些 Spring Security 的冷門用法,不是爲了顯擺,只是但願你們可以從不一樣的角度加深對 Spring Security 的理解,這些冷門的用法很是有助於你們理解 Spring Security 的內部工做原理。我原本能夠純粹的去講源碼,講原理,可是那樣太枯燥了,因此我會盡可能經過一些小的案例來幫助你們理解源碼,這些案例的目的只是爲了幫助你們理解 Spring Security 源碼,僅此而已!因此請你們不要和我擡槓這些用戶定義方式沒用!java

好啦,我今天要給你們表演一個絕活,就是花式定義用戶對象。但願你們經過這幾個案例,可以更好的理解 ProviderManager 的工做機制。git

本文內容和上篇文章【深刻理解 AuthenticationManagerBuilder 【源碼篇】】內容強關聯,因此強烈建議先學習下上篇文章內容,再來看本文,就會好理解不少。github

1.絕活一

先來看以下一段代碼:spring

@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,因此這裏前先後後至關於一共有三個用戶,那麼咱們登陸時候,使用哪一個用戶能夠登陸成功呢?app

先說結論:ide

  • 若是登陸地址是 /foo/login,那麼經過 sang 和 javaboy 兩個用戶能夠登陸成功。
  • 若是登陸地址是 /bar/login,那麼經過 sang 和 江南一點雨 兩個用戶能夠登陸成功。

也就是說,那個全局的,公共的 UserDetailsService 老是有效的,而針對不一樣過濾器鏈配置的 UserDetailsService 則只針對當前過濾器鏈生效。oop

鬆哥這裏爲了方便,使用了基於內存的 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

若是小夥伴們以爲有收穫,記得點個在看鼓勵下鬆哥哦~

相關文章
相關標籤/搜索