SpringSecurity過濾器鏈和認證過程y源碼分析

介紹
SpringSecurity核心功能:認證(身份校驗,你是誰),授權(你能幹什麼),攻擊防護(防止僞造身份)

一、原理過濾器鏈

è¿éåå¾çæè¿°
REST API:相當於應用的controller,用戶的增刪該查的一些服務 
Spring Security過濾器鏈:這個是最核心的部分,相當於一組Filter,請求和響應都會經過過濾器,這些過濾器在系統啓動的時候,Spring boot會自動把它們都配置進去 
綠色部分: 
代表過濾器,每個方塊都代表一個過濾器,UsernamePasswordAuthenticationFilter用來處理表單登錄的,BasicAuthenticationFilter用來處理HTTPBasic登錄的 
綠色部分過濾器主要功能:檢查當前的請求裏面是不是有過這個過濾器所需要的信息,比如說:UsernamePasswordAuthenticationFilter這個過濾器來說:首先檢查當前的請求是不是一個登錄請求,然後再這個請求裏面帶沒帶用戶名和密碼,如果帶了用戶名和密碼,這個過濾器就會用用戶名和密碼嘗試去登錄,如果這個請求裏面沒有帶用戶名和密碼,繼續進行下面的過濾器,下一個過濾器比如說是BasicAuthenticationFilter,這個過濾器會檢查請求的請求頭中是不是有Basic開頭的authentication的信息,如果有的話,會嘗試拿出來做basic流的字節碼然後從中取出用戶名和密碼嘗試做登錄,springsecurity還提供了許多其他的認證方式,任何一個過濾器,成功的完成用戶登錄以後,都會再這個請求上做個標記,表明這個用戶已經認證成功了,請求經歷過這些過濾器後,就會到達這個寫橘黃色的攔截器上比如FilterSecurityInterceptor,這個攔截器是整個Spring Security過濾器的最後一環,是最終的守門人,在它身後是我們自己寫的controller的rest服務,在該攔截器中,它會去決定當前的請求能不能去訪問後面真正的服務,判斷的依據類似於下面: 

這裏寫圖片描述 
 
上面的配置表明(任何請求都需要認證),就會去判斷當前的請求是不是經過了前面某一個過濾器的身份認證,可以定義一些特別複雜的規則,將這些規則放到Filter SecurityInterceptor裏面,這個過濾器會根據這些規則去判斷,判斷的結果是通過還是不同過,通過的話,請求就過去了,就能訪問最終的服務了,如果不過,根據不過的原因會拋出不同的異常,比如沒有認證的異常,或者該請求是給VIP用戶用的,您不是VIP用戶。異常拋出去以後,這個過濾器的前面還有藍色的過濾器ExceptionTranslationFilter,這個過濾器它的作用就是用來捕獲橘黃色過濾器所拋出的異常,藍色的過濾器會根據拋出的異常做相應的處理,比如說你沒有登錄就訪問,它會根據前面的配置引導你先去登錄,比如我前面配置了UsernamePasswordFilter過濾器,這樣就會把用戶引導到登錄頁面上,比如說我前面配置了BasicAuthenticationFilter這個過濾器,這樣就會在瀏覽器彈出一個窗口的調用,提醒用戶輸入用戶名和密碼,這就是Spring Security的一個基本原理,它應用的所用功能特性都是建立在這個過濾器鏈上的 
在過濾器鏈上,綠色的部分是可以通過配置來實現的,如下: 
 
上面是兩種認證方式: 
一種是httpbasic認證;還有一種是fromlogin表單認證 
注意:過濾器鏈上的順序不能更改,可以自定義加入其他過濾器。

二、認證過程

1.使用ctrl+shift+n組合鍵查找UsernamePasswordAuthenticationFilter過濾器,該過濾器是用來處理用戶認證邏輯的,進入後如圖:

(1)可以看到它默認的登錄請求url是"/login",並且只允許POST方式的請求

(2)obtainUsername()方法點進去發現它默認是根據參數名爲"username"和"password"來獲取用戶名和密碼的

(3)通過構造方法實例化一個UsernamePasswordAuthenticationToken對象,此時調用的是UsernamePasswordAuthenticationToken的兩個參數的構造函數,如圖:

其中super(null)調用的是父類的構造方法,傳入的是權限集合,因爲目前還沒有認證通過,所以不知道有什麼權限信息,這裏設置爲null,然後將用戶名和密碼分別賦值給principal和credentials,同樣因爲此時還未進行身份認證,所以setAuthenticated(false)

(4)setDetails(request, authRequest)是將當前的請求信息設置到UsernamePasswordAuthenticationToken中

(5)通過調用getAuthenticationManager()來獲取AuthenticationManager,通過調用它的authenticate方法來查找支持該token(UsernamePasswordAuthenticationToken)認證方式的provider,然後調用該provider的authenticate方法進行認證

2.AuthenticationManager是用來管理AuthenticationProvider的接口,通過查找後進入,然後使用ctrl+H組合鍵查看它的繼承關係,找到ProviderManager實現類,它實現了AuthenticationManager接口,查看它的authenticate方法,它裏面有段這樣的代碼:

for (AuthenticationProvider provider : getProviders()) {
        if (!provider.supports(toTest)) {
            continue;
        }
    ...
    try {
            result = provider.authenticate(authentication);
    ...
    }
}
通過for循環遍歷AuthenticationProvider對象的集合,找到支持當前認證方式的AuthenticationProvider,找到之後調用該AuthenticationProvider的authenticate方法進行認證處理:

result = provider.authenticate(authentication);
3.AuthenticationProvider接口,就是進行身份認證的接口,它裏面有兩個方法:authenticate認證方法和supports是否支持某種類型token的方法,通過ctrl+h查看繼承關係,找到AbstractUserDetailsAuthenticationProvider抽象類,它實現了AuthenticationProvider接口,它的supports方法如下:

    public boolean supports(Class<?> authentication) {
        return (UsernamePasswordAuthenticationToken.class
            .isAssignableFrom(authentication));
        }
說明它是支持UsernamePasswordAuthenticationToken類型的AuthenticationProvider

再看它的authenticate認證方法,其中有一段這樣的代碼:

boolean cacheWasUsed = true;
    UserDetails user = this.userCache.getUserFromCache(username);
 
    if (user == null) {
        cacheWasUsed = false;
 
        try {
            user = retrieveUser(username,
                    (UsernamePasswordAuthenticationToken) authentication);
        }
    ...
    }
如果從緩存中沒有獲取到UserDetails,那麼它調用retrieveUser方法來獲取用戶信息UserDetails,這裏的retrieveUser是抽象方法,等一會我們看它的子類實現。

用戶信息UserDetails是個接口,我們進入查看,它包含以下6個接口方法:

Collection<? extends GrantedAuthority> getAuthorities();//獲取權限集合
String getPassword();  //獲取密碼
String getUsername();   //獲取用戶名
boolean isAccountNonExpired(); //賬戶未過期
boolean isAccountNonLocked();   //賬戶未鎖定
boolean isCredentialsNonExpired(); //密碼未過期
boolean isEnabled();    //賬戶可用
查看它的繼承關係發現User類實現了該接口,並實現了該接口的所有方法

接着AbstractUserDetailsAuthenticationProvider往下看,找到下面的代碼:

    preAuthenticationChecks.check(user);
    additionalAuthenticationChecks(user,
                (UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks預檢查,在最下面的內部類DefaultPreAuthenticationChecks中可以看到,它會檢查上面提到的三個boolean方法,即檢查賬戶未鎖定、賬戶可用、賬戶未過期,如果上面的方法只要有一個返回false,就會拋出異常,那麼認證就會失敗。

additionalAuthenticationChecks是附加檢查,是個抽象方法,等下看子類的具體實現。

下面還有個postAuthenticationChecks.check(user)後檢查,在最下面的DefaultPostAuthenticationChecks內部類中可以看到,它會檢查密碼未過期,如果爲false就會拋出異常

如果上面的檢查都通過並且沒有異常,表示認證通過,會調用下面的方法:

createSuccessAuthentication(principalToReturn, authentication, user);
跟進發現此時通過構造方法實例化對象UsernamePasswordAuthenticationToken時,調用的是三個參數的構造方法:

    public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
        Collection<? extends GrantedAuthority> authorities) {
    super(authorities);
    this.principal = principal;
    this.credentials = credentials;
    super.setAuthenticated(true); // must use super, as we override
}
此時會調用父類的構造方法設置權限信息,並調用父類的setAuthenticated(true)方法,到這裏就表示認證通過了。

下面我們看看AbstractUserDetailsAuthenticationProvider的子類,同ctrl+h可查看繼承關係,找到DaoAuthenticationProvider

4.DaoAuthenticationProvider類

(1)查看additionalAuthenticationChecks附加檢查方法,它主要是檢查用戶密碼的正確性,如果密碼爲空或者錯誤都會拋出異常

(2)獲取用戶信息UserDetails的retrieveUser方法,主要看下面這段代碼:

UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
它是調用了getUserDetailsService先獲取到UserDetailsService對象,通過調用UserDetailsService對象的loadUserByUsername方法獲取用戶信息UserDetails

找到UserDetailsService,發現它是一個接口,查看繼承關係,有很多實現,都是spring-security提供的實現類,並不滿足我們的需要,我們想自己制定獲取用戶信息的邏輯,所以我們可以實現這個接口。比如從我們的數據庫中查找用戶信息

5.SecurityContextPersistenceFilter過濾器

那麼用戶認證成功之後,又是怎麼保存認證信息的呢,在下一次請求過來是如何判斷該用戶是否已經認證了呢?

請求進來時會經過SecurityContextPersistenceFilter過濾器,進入SecurityContextPersistenceFilter過濾器並找到以下代碼:

SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
從session中獲取SecurityContext對象,如果沒有就實例化一個SecurityContext對象

SecurityContextHolder.setContext(contextBeforeChainExecution);
將SecurityContext對象設置到SecurityContextHolder中

chain.doFilter(holder.getRequest(), holder.getResponse());
表示放行,執行下一個過濾器

執行完後面的過濾並經過servlet處理之後,響應給瀏覽器之前再次經過此過濾器。查看以下代碼:

SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
SecurityContextHolder.clearContext();
this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
通過SecurityContextHolder獲取SecurityContext對象,然後清除SecurityContext,最後將獲取的SecurityContext對象放入session中

其中SecurityContextHolder是與ThreadLocal綁定的,即本線程內所有的方法都可以獲得SecurityContext對象,而SecurityContext對象中包含了Authentication對象,即用戶的認證信息,spring-security判斷用戶是否認證主要是根據SecurityContext中的Authentication對象來判斷。Authentication對象的詳細信息如圖:

源碼分析:UsernamePasswordAuthenticationFilter和SocialAuthenticationFilter用的都是通過調用getAuthenticationManager()來獲取AuthenticationManager,

代碼:Authentication success = getAuthenticationManager().authenticate(token);

傳入不同的token: SocialAuthenticationToken、UsernamePasswordAuthentication

AuthenticationManager選擇不同的AuthenticationProvider認證處理

原文鏈接:https://blog.csdn.net/abcwanglinyong/article/details/80981389

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章