一、認證處理流程說明
原理圖
1.在前臺輸入完用戶名密碼之后,會進入UsernamePasswordAuthenticationFilter類中去獲取用戶名和密碼,然后去構建一個UsernamePasswordAuthenticationToken對象。
這個對象實現了Authentication接口,Authentication接口封裝了驗證信息,在調用UsernamePasswordAuthenticationToken的構造函數的時候先調用父類AbstractAuthenticationToken的構造方法,傳遞一個null,因為在認證的時候并不知道這個用戶有什么權限。之后去給用戶名密碼賦值,最后有一個setAuthenticated(false)方法,代表存進去的信息是否經過了身份認證,源碼如下:
2.實例化UsernamePasswordAuthenticationToken之后調用了setDetails(request,authRequest)將請求的信息設到UsernamePasswordAuthenticationToken中去,包括ip、session等內容
3.然后去調用AuthenticationManager,AuthenticationManager本身不包含驗證的邏輯,它的作用是用來管理AuthenticationProvider。
authenticate這個方法是在ProviderManager類上的,這個類實現了AuthenticationManager接口,在authenticate方法中有一個for循環,去拿到所有的AuthenticationProvider,真正校驗的邏輯是寫在AuthenticationProvider中的,為什么是一個集合去進行循環?是因為不同的登陸方式認證邏輯是不一樣的,可能是微信等社交平臺登陸,也可能是用戶名密碼登陸。AuthenticationManager其實是將AuthenticationProvider收集起來,然后登陸的時候挨個去AuthenticationProvider中問你這種驗證邏輯支不支持此次登陸的方式,根據傳進來的Authentication類型會挑出一個適合的provider來進行校驗處理。
然后去調用provider的驗證方法authenticate方法,authenticate是DaoAuthenticationProvider類中的一個方法,DaoAuthenticationProvider繼承了AbstractUserDetailsAuthenticationProvider。實際上authenticate的校驗邏輯寫在了AbstractUserDetailsAuthenticationProvider抽象類中,首先實例化UserDetails對象,調用了retrieveUser方法獲取到了一個user對象,retrieveUser是一個抽象方法。
DaoAuthenticationProvider實現了retrieveUser方法,在實現的方法中實例化了UserDetails對象
也就是相當于自定義驗證邏輯的那個類,去實現UserDetailService類,這個返回結果就是我們自己在數據庫中根據username查詢出來的用戶信息。在AbstractUserDetailsAuthenticationProvider中如果沒拿到信息就會拋出異常,如果查到了就會去調用preAuthenticationChecks的check方法去進行預檢查。
在預檢查中進行了三個檢查,因為UserDetail類中有四個布爾類型,去檢查其中的三個,用戶是否鎖定、用戶是否過期,用戶是否可用。
預檢查之后緊接著去調用了additionalAuthenticationChecks方法去進行附加檢查,這個方法也是一個抽象方法,在DaoAuthenticationProvider中去具體實現,在里面進行了加密解密去校驗當前的密碼是否匹配。
4.如果通過了預檢查和附加檢查,還會進行厚檢查,檢查4個布爾中的最后一個。所有的檢查都通過,則認為用戶認證是成功的。用戶認證成功之后,會將這些認證信息和user傳遞進去,調用createSuccessAuthentication方法.
在這個方法中同樣會實例化一個user,但是這個方法不會調用之前傳兩個參數的函數,而是會調用三個參數的構造函數。這個時候,在調super的構造函數中不會再傳null,會將authorities權限設進去,之后將用戶密碼設進去,最后setAuthenticated(true),代表驗證已經通過。
最后創建一個authentication會沿著驗證的這條線返回回去。如果驗證成功,則在這條路中調用我們系統的業務邏輯。如果在任何一處發生問題,就會拋出異常,調用我們自己定義的認證失敗的處理器。
二、認證結果如何在多個請求之間共享
問題:它是什么時候,把什么東西放到了session中,什么時候在session中讀出來。
原理圖:
在驗證成功之后,其中會調用AbstractAuthenticationFilter中的successfulAuthentication方法,在這個方法最后會調用我們自定義的successHandle登陸成功r處理器,在調用這個方法之前會調用SecurityContextHolder.getContext()的setAuthentication方法,會將我們驗證成功的那個Authentication放到SecurityContext中,然后再放到SecurityContextHolder中。SecurityContextImpl中只是重寫了hashcode方法和equals方法去保證Authentication的唯一。
SecurityContextHolder是ThreadLocal的一個封裝,ThreadLocal是線程綁定的一個map,在同一個線程里在這個方法里往ThreadLocal里設置的變量是可以在另一個線程中讀取到的。它是一個線程級的全局變量,在一個線程中操作ThreadLocal中的數據會影響另一個線程。也就是說創建成功之后,塞進去,此次登陸所有的請求都會通過SecurityContextPersisenceFilter去SecurityContextHolder拿那個Authentication。SecurityContextHolder在整個過濾器的最前面。
當請求進來的時候,會先經過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”得到用戶的身份信息
改進
也可以這樣寫
@GetMapping("/me")
public Object getCurrentUser(Authentication authentication){
return authentication;
}
同樣也可以拿到用戶的身份信息,但是如果我只想拿到用戶名不想拿到那么多一長串怎么辦?
代碼可以這樣寫:
@GetMapping("/me")
public Object getCurrentUser(@AuthenticationPrincipal UserDetails userDetails){
return userDetails;
}
然后重新登錄訪問:可以看到
其實我只拿到了Principal對象。