1. 程式人生 > >SpringSecurity認證流程

SpringSecurity認證流程

SpringSecurity基本原理

在之前的文章《SpringBoot + Spring Security 基本使用及個性化登入配置》中對SpringSecurity進行了簡單的使用介紹,基本上都是對於介面的介紹以及功能的實現。 這一篇文章嘗試從原始碼的角度來上對使用者認證流程做一個簡單的分析。 在具體分析之前,我們可以先看看SpringSecurity的大概原理: SpringSecurity基本原理 其實比較簡單,主要是通過一系列的Filter對請求進行攔截處理。

認證處理流程說明

我們直接來看UsernamePasswordAuthenticationFilter類,

public class UsernamePasswordAuthenticationFilter
extends AbstractAuthenticationProcessingFilter // 登入請求認證 public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
// 判斷是否是POST請求 if (this.postOnly && !request.getMethod().equals("POST")) { throw
new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } else { // 獲取使用者,密碼 String username = this.obtainUsername(request); String password = this.obtainPassword(request); if (username == null) { username = ""
; } if (password == null) { password = ""; } username = username.trim(); // 生成Token, UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); this.setDetails(request, authRequest); // 進一步驗證 return this.getAuthenticationManager().authenticate(authRequest); } } }

attemptAuthentication方法中,主要是進行username和password請求值的獲取,然後再生成一個UsernamePasswordAuthenticationToken 物件,進行進一步的驗證。 不過我們可以先看看UsernamePasswordAuthenticationToken 的構造方法

public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
    // 設定空的許可權
    super((Collection)null);
    this.principal = principal;
    this.credentials = credentials;
    // 設定是否通過了校驗
    this.setAuthenticated(false);
}

其實UsernamePasswordAuthenticationToken是繼承於Authentication,該物件在上一篇文章中有提到過,它是處理登入成功回撥方法中的一個引數,裡面包含了使用者資訊、請求資訊等引數。

所以接下來我們看

this.getAuthenticationManager().authenticate(authRequest);

這裡有一個AuthenticationManager,但是真正呼叫的是ProviderManager

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
    public Authentication authenticate(Authentication authentication) throws AuthenticationException { 
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        Authentication result = null;
        boolean debug = logger.isDebugEnabled();
        Iterator var6 = this.getProviders().iterator();

        while(var6.hasNext()) {
            AuthenticationProvider provider = (AuthenticationProvider)var6.next();
            // 1.判斷是否有provider支援該Authentication
            if (provider.supports(toTest)) {
                // 2. 真正的邏輯判斷
                result = provider.authenticate(authentication);
            }
    }
}
  1. 這裡首先通過provider判斷是否支援當前傳入進來的Authentication,目前我們使用的是UsernamePasswordAuthenticationToken,因為除了帳號密碼登入的方式,還會有其他的方式,比如SocialAuthenticationToken。
  2. 根據我們目前所使用的UsernamePasswordAuthenticationToken,provider對應的是DaoAuthenticationProvider
public Authentication authenticate(Authentication authentication) throws AuthenticationException {   
    UserDetails user = this.userCache.getUserFromCache(username);
    if (user == null) {
        cacheWasUsed = false;
        // 1.去獲取UserDetails
        user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
    }

    try {
        // 2.使用者資訊預檢查
        this.preAuthenticationChecks.check(user);
        // 3.附加的檢查(密碼檢查)
        this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
    } catch (AuthenticationException var7) {       
    }
    // 4.最後的檢查
    this.postAuthenticationChecks.check(user);
    // 5.返回真正的經過認證的Authentication 
    return this.createSuccessAuthentication(principalToReturn, authentication, user);
}
  1. 去呼叫自己實現的UserDetailsService,返回UserDetails
  2. 對UserDetails的資訊進行校驗,主要是帳號是否被凍結,是否過期等
  3. 對密碼進行檢查,這裡呼叫了PasswordEncoder
  4. 檢查UserDetails是否可用。
  5. 返回經過認證的Authentication

這裡的兩次對UserDetails的檢查,主要就是通過它的四個返回boolean型別的方法。 經過資訊的校驗之後,通過UsernamePasswordAuthenticationToken的構造方法,返回了一個經過認證的Authentication。

拿到經過認證的Authentication之後,會再去呼叫successHandler。或者未通過認證,去呼叫failureHandler。

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

再完成了使用者認證處理流程之後,我們思考一下是如何在多個請求之間共享這個認證結果的呢? 因為沒有做關於這方面的配置,所以可以聯想到預設的方式應該是在session中存入了認證結果。 那麼是什麼時候存放入session中的呢? 我們可以接著認證流程的原始碼往後看,在通過attemptAuthentication方法後,如果認證成功,會呼叫successfulAuthentication,該方法中,不僅呼叫了successHandler,還有一行比較重要的程式碼

SecurityContextHolder.getContext().setAuthentication(authResult);

SecurityContextHolder是對於ThreadLocal的封裝。 ThreadLocal是一個執行緒內部的資料儲存類,通過它可以在指定的執行緒中儲存資料,資料儲存以後,只有在指定執行緒中可以獲取到儲存的資料,對於其他執行緒來說則無法獲取到資料。 更多的關於ThreadLocal的原理可以看看我以前的文章。

一般來說同一個介面的請求和返回,都會是在一個執行緒中完成的。我們在SecurityContextHolder中放入了authResult,再其他地方也可以取出來的。 最後就是在SecurityContextPersistenceFilter中取出了authResult,並存入了session

SecurityContextPersistenceFilter也是一個過濾器,它處於整個Security過濾器鏈的最前方,也就是說開始驗證的時候是最先通過該過濾器,驗證完成之後是最後通過。

獲取認證使用者資訊

/**
 * 獲取當前登入的使用者
 * @return 完整的Authentication
 */
@GetMapping("/me1")
public Object currentUser() {
    return SecurityContextHolder.getContext().getAuthentication();
}

@GetMapping("/me2")
public Object currentUser(Authentication authentication) {
    return authentication;
}

/**
 * @param userDetails
 * @return 只包含了userDetails
 */
@GetMapping("/me3")
public Object cuurentUser(@AuthenticationPrincipal UserDetails userDetails) {
    return userDetails;
}
					<link href="https://csdnimg.cn/release/phoenix/mdeditor/markdown_views-fe496b5a0d.css" rel="stylesheet">
            </div>