Spring Security認證器實現

2022-06-24 06:00:49

一些許可權框架一般都包含認證器和決策器,前者處理登陸驗證,後者處理存取資源的控制

Spring Security的登陸請求處理如圖

下面來分析一下是怎麼實現認證器的

攔截請求

首先登陸請求會被UsernamePasswordAuthenticationFilter攔截,這個過濾器看名字就知道是一個攔截使用者名稱密碼的攔截器

主要的驗證是在attemptAuthentication()方法裡,他會去獲取在請求中的使用者名稱密碼,並且建立一個該使用者的上下文,然後在去執行一個驗證過程

String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
//建立上下文
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);

可以看看UsernamePasswordAuthenticationToken這個類,他是繼承了AbstractAuthenticationToken,然後這個父類別實現了Authentication

由這個類的方法和屬性可得知他就是儲存使用者驗證資訊的,認證器的主要功能應該就是驗證完成後填充這個類

回到UsernamePasswordAuthenticationToken中,在上面建立的過程了可以發現

public UsernamePasswordAuthenticationToken(Object principal,Object credentials){
    super(null);
    this.principal=principal;
    this.credentials=credentials;
    //還沒認證
    setAuthenticated(false);
}

還有一個super(null)的處理,因為剛進來是還不知道有什麼許可權的,設定null是初始化一個空的許可權

//許可權利集合
private final Collection<GrantedAuthority> authorities;
//空的集合
public static final List<GrantedAuthority> NO_AUTHORITIES = Collections.emptyList();
//初始化
if (authorities == null) {
    this.authorities = AuthorityUtils.NO_AUTHORITIES;
    return;
}

那麼後續認證完還會把許可權設定儘量,此時可以看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
}

在看原始碼的過程中,註釋一直在強調這些上下文的填充和設定都應該是由AuthenticationManager或者AuthenticationProvider的實現類去操作

驗證過程

接下來會把球踢給AuthenticationManager,但他只是個介面

/**
 * Attempts to authenticate the passed {@link Authentication} object, returning a
 * fully populated <code>Authentication</code> object (including granted authorities)
 * if successful.
 **/
public interface AuthenticationManager {
    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;
}

註釋也寫的很清楚了,認證完成後會填充Authentication

接下來會委託給ProviderManager,因為他實現了AuthenticationManager

剛進來看authenticate()方法會發現他先遍歷了一個List<AuthenticationProvider>集合

/**
 * Indicates a class can process a specific Authentication 
 **/
public interface AuthenticationProvider {
    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;
    //支不支援特定型別的authentication
    boolean supports(Class<?> authentication);
}

實現這個類就可以處理不同型別的Authentication,比如上邊的UsernamePasswordAuthenticationToken,對應的處理類是AbstractUserDetailsAuthenticationProvider,為啥知道呢,因為在這個supports()

public boolean supports(Class<?> authentication) {
		return (UsernamePasswordAuthenticationToken.class
				.isAssignableFrom(authentication));
}

注意到這個是抽象類,實際的處理方法是在他的子類DaoAuthenticationProvider裡,但是最重要的authenticate()方法子類好像沒有繼承,看看父類別是怎麼實現這個方法的

  1. 首先是繼續判斷Authentication是不是特定的類

     Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
         () -> messages.getMessage(
         "AbstractUserDetailsAuthenticationProvider.onlySupports",
         "Only UsernamePasswordAuthenticationToken is supported"));
    
  2. 查詢根據使用者名稱使用者,這次就是到了子類的方法了,因為這個方法是抽象的

     user=retrieveUser(username,
         (UsernamePasswordAuthenticationToken)authentication);
    

    接著DaoAuthenticationProvider會呼叫真正實現查詢使用者的類UserDetailsService

    UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
    

    UserDetailsService這個類資訊就不陌生了,我們一般都會去實現這個類來自定義查詢使用者的方式,查詢完後會返回一個UserDetails,當然也可以繼承這個類來擴充套件想要的欄位,主要填充的是許可權資訊和密碼

  3. 檢驗使用者,如果獲取到的UserDetails是null,則拋異常,不為空則繼續校驗

    //檢驗使用者合法性
    preAuthenticationChecks.check(user);
    //校驗密碼
    additionalAuthenticationChecks(user,
    (UsernamePasswordAuthenticationToken) authentication);
    

    第一個教育是判斷使用者的合法性,就是判斷UserDetails裡的幾個欄位

    //賬號是否過期
    boolean isAccountNonExpired();
    //賬號被鎖定或解鎖狀態。
    boolean isAccountNonLocked();
    //密碼是否過期
    boolean isCredentialsNonExpired();
    //是否啟用
    boolean isEnabled();
    

    第二個則是由子類實現的,判斷從資料庫獲取的密碼和請求中的密碼是否一致,因為用的登陸方式是根據使用者名稱稱登陸,所以有檢驗密碼的步驟

     String presentedPassword = authentication.getCredentials().toString();
     if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
         logger.debug("Authentication failed: password does not match stored value");
         throw new BadCredentialsException(messages.getMessage(
         "AbstractUserDetailsAuthenticationProvider.badCredentials",
         "Bad credentials"));
     }
    

    需要主要的是請求中的密碼是被加密過的,所以從資料庫獲取到的密碼也應該是被加密的

    注意到當完成校驗的時候會把資訊放入快取

    //當沒有從快取中獲取到值時,這個欄位會被設定成false
    if (!cacheWasUsed) {
    			this.userCache.putUserInCache(user);
     }
     //下次進來的時候回去獲取
     UserDetails user = this.userCache.getUserFromCache(username);
    
    

    如果是從快取中獲取,也是會走檢驗邏輯的

    最後完成檢驗,並填充一個完整的Authentication

    return createSuccessAuthentication(principalToReturn, authentication, user);
    

由上述流程來看,Security的檢驗過程還是比較清晰的,通過AuthenticationManager來委託給ProviderManager,在通過具體的實現類來處理請求,在這個過程中,將查詢使用者的實現和驗證程式碼分離開來

整個過程看著像是策略模式,後邊將變化的部分抽離出來,實現解耦

返回完整的Authentication

前邊提到的認證成功會呼叫createSuccessAuthentication()方法,裡邊的內容很簡單

UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
     principal, authentication.getCredentials(),
     authoritiesMapper.mapAuthorities(user.getAuthorities()));
     result.setDetails(authentication.getDetails());
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
        }

這次往supe裡放了許可權集合,父類別的處理是判斷裡邊的許可權有沒有空的,沒有則轉換為唯讀集合

for (GrantedAuthority a : authorities) {
    if (a == null) {
        throw new IllegalArgumentException(
        "Authorities collection cannot contain any null elements");
    }
}
ArrayList<GrantedAuthority> temp = new ArrayList<>(
authorities.size());
temp.addAll(authorities);
this.authorities = Collections.unmodifiableList(temp);

收尾工作

回到ProviderManager裡的authenticate方法,當我們終於從

result = provider.authenticate(authentication);

走出來時,後邊還有什麼操作

  1. 將返回的使用者資訊負責給當前的上下文
   if (result != null) {
   	copyDetails(authentication, result);
   	break;
   }
  1. 刪除敏感資訊

    ((CredentialsContainer) result).eraseCredentials();
    

    這個過程會將一些欄位設定為null,可以實現eraseCredentials()方法來自定義需要刪除的資訊

最後返回到UsernamePasswordAuthenticationFilter中通過過濾

結論

這就是Spring Security實現認證的過程了

通過實現自己的上下文Authentication和處理類AuthenticationProvider以及具體的查詢使用者的方法就可以自定義自己的登陸實現
具體可以看Spring Security自定義認證器