SpringSecurity認證流程原始碼詳解

weixin_34116110發表於2019-01-10

一、認證處理流程說明

原理圖

15200008-35c6fcbac0468fae.png
認證處理流程說明原理圖

1.在前臺輸入完使用者名稱密碼之後,會進入UsernamePasswordAuthenticationFilter類中去獲取使用者名稱和密碼,然後去構建一個UsernamePasswordAuthenticationToken物件。
15200008-c4b90f544d6d47bc.png
構建一個UsernamePasswordAuthenticationToken物件

這個物件實現了Authentication介面,Authentication介面封裝了驗證資訊,在呼叫UsernamePasswordAuthenticationToken的建構函式的時候先呼叫父類AbstractAuthenticationToken的構造方法,傳遞一個null,因為在認證的時候並不知道這個使用者有什麼許可權。之後去給使用者名稱密碼賦值,最後有一個setAuthenticated(false)方法,代表存進去的資訊是否經過了身份認證,原始碼如下:
15200008-53e901c01e685e7a.png
UsernamePasswordAuthenticationToken原始碼

2.例項化UsernamePasswordAuthenticationToken之後呼叫了setDetails(request,authRequest)將請求的資訊設到UsernamePasswordAuthenticationToken中去,包括ip、session等內容
15200008-1ed8fd79b34721a0.png
setDetails

3.然後去呼叫AuthenticationManager,AuthenticationManager本身不包含驗證的邏輯,它的作用是用來管理AuthenticationProvider。


15200008-53a97b5e389cff93.png
5.png

authenticate這個方法是在ProviderManager類上的,這個類實現了AuthenticationManager介面,在authenticate方法中有一個for迴圈,去拿到所有的AuthenticationProvider,真正校驗的邏輯是寫在AuthenticationProvider中的,為什麼是一個集合去進行迴圈?是因為不同的登陸方式認證邏輯是不一樣的,可能是微信等社交平臺登陸,也可能是使用者名稱密碼登陸。AuthenticationManager其實是將AuthenticationProvider收集起來,然後登陸的時候挨個去AuthenticationProvider中問你這種驗證邏輯支不支援此次登陸的方式,根據傳進來的Authentication型別會挑出一個適合的provider來進行校驗處理。


15200008-cb006def5ed1ed2b.png
6.png

然後去呼叫provider的驗證方法authenticate方法,authenticate是DaoAuthenticationProvider類中的一個方法,DaoAuthenticationProvider繼承了AbstractUserDetailsAuthenticationProvider。實際上authenticate的校驗邏輯寫在了AbstractUserDetailsAuthenticationProvider抽象類中,首先例項化UserDetails物件,呼叫了retrieveUser方法獲取到了一個user物件,retrieveUser是一個抽象方法。
15200008-b8fb791c4a91aca2.png
7.png

DaoAuthenticationProvider實現了retrieveUser方法,在實現的方法中例項化了UserDetails物件


15200008-e4c329337b24b672.png
8.png

也就是相當於自定義驗證邏輯的那個類,去實現UserDetailService類,這個返回結果就是我們自己在資料庫中根據username查詢出來的使用者資訊。在AbstractUserDetailsAuthenticationProvider中如果沒拿到資訊就會丟擲異常,如果查到了就會去呼叫preAuthenticationChecks的check方法去進行預檢查。
15200008-2fe431aa33ded3c7.png
9.png

在預檢查中進行了三個檢查,因為UserDetail類中有四個布林型別,去檢查其中的三個,使用者是否鎖定、使用者是否過期,使用者是否可用。


15200008-58348fedf4850572.png
10.png

預檢查之後緊接著去呼叫了additionalAuthenticationChecks方法去進行附加檢查,這個方法也是一個抽象方法,在DaoAuthenticationProvider中去具體實現,在裡面進行了加密解密去校驗當前的密碼是否匹配。


15200008-320903b04347b523.png
11.png

4.如果通過了預檢查和附加檢查,還會進行厚檢查,檢查4個布林中的最後一個。所有的檢查都通過,則認為使用者認證是成功的。使用者認證成功之後,會將這些認證資訊和user傳遞進去,呼叫createSuccessAuthentication方法.


15200008-38b497c391d88bef.png
12.png

在這個方法中同樣會例項化一個user,但是這個方法不會呼叫之前傳兩個引數的函式,而是會呼叫三個引數的建構函式。這個時候,在調super的建構函式中不會再傳null,會將authorities許可權設進去,之後將使用者密碼設進去,最後setAuthenticated(true),代表驗證已經通過。
15200008-db869bc4fbf62359.png
13.png

最後建立一個authentication會沿著驗證的這條線返回回去。如果驗證成功,則在這條路中呼叫我們系統的業務邏輯。如果在任何一處發生問題,就會丟擲異常,呼叫我們自己定義的認證失敗的處理器。

二、認證結果如何在多個請求之間共享

問題:它是什麼時候,把什麼東西放到了session中,什麼時候在session中讀出來。
原理圖:

15200008-d32f7ced55604153.png
原理

在驗證成功之後,其中會呼叫AbstractAuthenticationFilter中的successfulAuthentication方法,在這個方法最後會呼叫我們自定義的successHandle登陸成功r處理器,在呼叫這個方法之前會呼叫SecurityContextHolder.getContext()的setAuthentication方法,會將我們驗證成功的那個Authentication放到SecurityContext中,然後再放到SecurityContextHolder中。SecurityContextImpl中只是重寫了hashcode方法和equals方法去保證Authentication的唯一。
15200008-7fa3151be676095e.png
22.png

SecurityContextHolder是ThreadLocal的一個封裝,ThreadLocal是執行緒繫結的一個map,在同一個執行緒裡在這個方法裡往ThreadLocal裡設定的變數是可以在另一個執行緒中讀取到的。它是一個執行緒級的全域性變數,在一個執行緒中操作ThreadLocal中的資料會影響另一個執行緒。也就是說建立成功之後,塞進去,此次登陸所有的請求都會通過SecurityContextPersisenceFilter去SecurityContextHolder拿那個Authentication。SecurityContextHolder在整個過濾器的最前面。
15200008-66e8d77d8aa2480a.png
23.png

當請求進來的時候,會先經過SecurityContextPersisenceFilter,SecurityContextPersisenceFilter會去session中去查SecurityContext的驗證資訊,如果有,就把SecurityContext的驗證資訊放到執行緒裡直接返回回去,如果沒有則通過,去通過其他的過濾器,當請求處理完回來之後,SecurityContextHolder會去檢查當前執行緒中有沒有SecurityContext的驗證資訊,如果有,則將SecurityContext放到session中。通過這樣將不同的請求就可以從同一個session裡拿到驗證資訊。

簡單來說就是進來的時候檢查session,有認證資訊放到執行緒裡。出去的時候檢查執行緒,有認證資訊放到session裡。
因為整個請求和響應的過程都是在一個執行緒裡去完成的,所以線上程的其他位置隨時可以用SecurityContextHolder來拿到認證資訊。

三、獲取認證使用者資訊

其實使用SecurityContextHolder去獲取使用者的認證資訊的。
我在UserController上加入一個新的介面

@RestController
@RequestMapping("/user")
public class UserController {

    @GetMapping("/me")
    public Object getCurrentUser(){
        return SecurityContextHolder.getContext().getAuthentication();
    }

然後我在瀏覽器裡先去登入,然後訪問“/user/me”得到使用者的身份資訊


15200008-29e27059f072b57c.png
使用者的身份資訊

改進
也可以這樣寫

   @GetMapping("/me")
    public Object getCurrentUser(Authentication authentication){
        return authentication;
    }

同樣也可以拿到使用者的身份資訊,但是如果我只想拿到使用者名稱不想拿到那麼多一長串怎麼辦?
程式碼可以這樣寫:

@GetMapping("/me")
    public Object getCurrentUser(@AuthenticationPrincipal UserDetails userDetails){
        return userDetails;
    }

然後重新登入訪問:可以看到


15200008-4ae12bac61bfabd9.png
只拿到了Principal物件

其實我只拿到了Principal物件。

相關文章