1. 程式人生 > 程式設計 >詳解Spring Security認證流程

詳解Spring Security認證流程

前言

Spring Seuciry相關的內容看了實在是太多了,但總覺得還是理解地不夠鞏固,還是需要靠知識輸出做鞏固。

相關版本:

java: jdk 8 
spring-boot: 2.1.6.RELEASE 

過濾器鏈和認證過程

詳解Spring Security認證流程

一個認證過程,其實就是過濾器鏈上的一個綠色矩形Filter所要執行的過程。

基本的認證過程有三步驟:

  1. Filter攔截請求,生成一個未認證的Authentication,交由AuthenticationManager進行認證;
  2. AuthenticationManager的預設實現ProviderManager會通過AuthenticationProvider
    Authentication進行認證,其本身不做認證處理;
  3. 如果認證通過,則建立一個認證通過的Authentication返回;否則丟擲異常,以表示認證不通過。

要理解這個過程,可以從類UsernamePasswordAuthenticationFilterProviderManagerDaoAuthenticationProviderInMemoryUserDetailsManagerUserDetailsService實現類,由UserDetailsServiceAutoConfiguration預設配置提供)進行了解。只要建立一個含有spring-boot-starter-security

的springboot專案,在適當地打上斷點介面看到這個流程。

用認證部門進行講解

詳解Spring Security認證流程)

請求到前臺之後,負責該請求的前臺會將請求的內容封裝為一個Authentication物件交給認證管理部門認證管理部門僅管理認證部門,不做具體的認證操作,具體的操作由與該前臺相關的認證部門進行處理。當然,每個認證部門需要判斷Authentication是否為該部門負責,是則由該部門負責處理,否則交給下一個部門處理。認證部門認證成功之後會建立一個認證通過的Authentication返回。否則要麼丟擲異常表示認證不通過,要麼交給下一個部門處理。

如果需要新增認證型別,只要增加相應的前臺(Filter)和與該前臺(Filter)

想對應的認證部門(AuthenticationProvider)就即可,當然也可以增加一個與已有前臺對應的認證部門認證部門會通過前臺生成的Authentication來判斷該認證是否由該部門負責,因而也許提供一個兩者相互認同的Authentication.

認證部門需要人員資料時,則可以從人員資料部門獲取。不同的系統有不同的人員資料部門,需要我們提供該人員資料部門,否則將拿到空白檔案。當然,人員資料部門不一定是唯一的,認證部門可以有自己的專屬資料部門

上圖還可以有如下的畫法:

詳解Spring Security認證流程

這個畫法可能會和FilterChain更加符合。每一個前臺其實就是FilterChain中的一個,客戶拿著請求逐個前臺請求認證,找到正確的前臺之後進行認證判斷。

前臺(Filter)

這裡的前臺Filter僅僅指實現認證的Filter,Spring Security Filter Chain中處理這些Filter還有其他的Filter,比如CsrfFilter。如果非要給角色給他們,那麼就當他們是保安人員吧。

Spring Security為我們提供了3個已經實現的Filter。UsernamePasswordAuthenticationFilterBasicAuthenticationFilterRememberMeAuthenticationFilter。如果不做任何個性化的配置,UsernamePasswordAuthenticationFilterBasicAuthenticationFilter會在預設的過濾器鏈中。這兩種認證方式也就是預設的認證方式。

UsernamePasswordAuthenticationFilter僅僅會對/login路徑生效,也就是說UsernamePasswordAuthenticationFilter負責釋出認證,釋出認證的介面為/login

public class UsernamePasswordAuthenticationFilter extends
    AbstractAuthenticationProcessingFilter {
  ...
  public UsernamePasswordAuthenticationFilter() {
    super(new AntPathRequestMatcher("/login","POST"));
  }
  ...
}

UsernamePasswordAuthenticationFilter為抽象類AbstractAuthenticationProcessingFilter的一個實現,而BasicAuthenticationFilter為抽象類BasicAuthenticationFilter的一個實現。這四個類的原始碼提供了不錯的前臺(Filter)實現思路。

AbstractAuthenticationProcessingFilter

AbstractAuthenticationProcessingFilter 提供了認證前後需要做的事情,其子類只需要提供實現完成認證的抽象方法attemptAuthentication(HttpServletRequest,HttpServletResponse)即可。使用AbstractAuthenticationProcessingFilter時,需要提供一個攔截路徑(使用AntPathMatcher進行匹配)來攔截對應的特定的路徑。

UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter作為實際的前臺,會將客戶端提交的username和password封裝成一個UsernamePasswordAuthenticationToken交給認證管理部門(AuthenticationManager)進行認證。如此,她的任務就完成了。

BasicAuthenticationFilter
前臺(Filter)只會處理含有Authorization的Header,且小寫化後的值以basic開頭的請求,否則該前臺(Filter)不負責處理。該Filter會從header中獲取Base64編碼之後的username和password,建立UsernamePasswordAuthenticationToken提供給認證管理部門(AuthenticationMananager)進行認證。

認證資料(Authentication)

前臺接到請求之後,會從請求中獲取所需的資訊,建立自家認證部門(AuthenticationProvider)所認識的認證資料(Authentication)認證部門(AuthenticationProvider)則主要是通過認證資料(Authentication)的型別判斷是否由該部門處理。

public interface Authentication extends Principal,Serializable {
  
  // 該principal具有的許可權。AuthorityUtils工具類提供了一些方便的方法。
  Collection<? extends GrantedAuthority> getAuthorities();
  // 證明Principal的身份的證書,比如密碼。
  Object getCredentials();
  // authentication request的附加資訊,比如ip。
  Object getDetails();
  // 當事人。在username+password模式中為username,在有userDetails之後可以為userDetails。
  Object getPrincipal();
  // 是否已經通過認證。
  boolean isAuthenticated();
  // 設定通過認證。
  void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

Authentication被認證之後,會儲存到一個thread-local的SecurityContext中。

// 設定
SecurityContextHolder.getContext().setAuthentication(anAuthentication);
// 獲取
Authentication existingAuth = SecurityContextHolder.getContext()
        .getAuthentication();

在寫前臺Filter的時候,可以先檢查SecurityContextHolder.getContext()中是否已經存在通過認證的Authentication了,如果存在,則可以直接跳過該Filter。已經通過驗證的Authentication建議設定為一個不可修改的例項。

目前從Authentication的類圖中看到的實現類,均為Authentication的抽象子類AbstractAuthenticationToken的實現類。實現類有好幾個,與前面的講到的Filter相關的有UsernamePasswordAuthenticationTokenRememberMeAuthenticationToken

AbstractAuthenticationTokenCredentialsContainerAuthentication的子類。實現了一些簡單的方法,但主要的方法還需要實現。該類的getName()方法的實現可以看到常用的principal類為UserDetailsAuthenticationPrincipalPrincial。如果有需要將物件設定為principal,可以考慮繼承這三個類中的一個。

public String getName() {
  if (this.getPrincipal() instanceof UserDetails) {
    return ((UserDetails) this.getPrincipal()).getUsername();
  }
  if (this.getPrincipal() instanceof AuthenticatedPrincipal) {
    return ((AuthenticatedPrincipal) this.getPrincipal()).getName();
  }
  if (this.getPrincipal() instanceof Principal) {
    return ((Principal) this.getPrincipal()).getName();
  }

  return (this.getPrincipal() == null) ? "" : this.getPrincipal().toString();
}

認證管理部門(AuthenticationManager)

AuthenticationManager是一個介面,認證Authentication,如果認證通過之後,返回的Authentication應該帶上該principal所具有的GrantedAuthority

public interface AuthenticationManager {
  Authentication authenticate(Authentication authentication)
      throws AuthenticationException;
}

該介面的註釋中說明,必須按照如下的異常順序進行檢查和丟擲:

  • DisabledException:賬號不可用
  • LockedException:賬號被鎖
  • BadCredentialsException:證書不正確

Spring Security提供一個預設的實現ProviderManager認證管理部門(ProviderManager)僅執行管理職能,具體的認證職能由認證部門(AuthenticationProvider)執行。

public class ProviderManager implements AuthenticationManager,MessageSourceAware,InitializingBean {
  ...

  public ProviderManager(List<AuthenticationProvider> providers) {
    this(providers,null);
  }

  public ProviderManager(List<AuthenticationProvider> providers,AuthenticationManager parent) {
    Assert.notNull(providers,"providers list cannot be null");
    this.providers = providers;
    this.parent = parent;
    checkState();
  }

  public Authentication authenticate(Authentication authentication)
      throws AuthenticationException {
    Class<? extends Authentication> toTest = authentication.getClass();
    AuthenticationException lastException = null;
    AuthenticationException parentException = null;
    Authentication result = null;
    Authentication parentResult = null;
    boolean debug = logger.isDebugEnabled();

    for (AuthenticationProvider provider : getProviders()) {
      // #1,檢查是否由該認證部門進行認證`AuthenticationProvider`
      if (!provider.supports(toTest)) {
        continue;
      }

      if (debug) {
        logger.debug("Authentication attempt using "
            + provider.getClass().getName());
      }

      try {
        // #2,認證部門進行認證
        result = provider.authenticate(authentication);

        if (result != null) {
          copyDetails(authentication,result);
          // #3,認證通過則不再進行下一個認證部門的認證,否則丟擲的異常被捕獲,執行下一個認證部門(AuthenticationProvider)
          break;
        }
      }
      catch (AccountStatusException e) {
        prepareException(e,authentication);
        // SEC-546: Avoid polling additional providers if auth failure is due to
        // invalid account status
        throw e;
      }
      catch (InternalAuthenticationServiceException e) {
        prepareException(e,authentication);
        throw e;
      }
      catch (AuthenticationException e) {
        lastException = e;
      }
    }

    if (result == null && parent != null) {
      // Allow the parent to try.
      try {
        result = parentResult = parent.authenticate(authentication);
      }
      catch (ProviderNotFoundException e) {
        // ignore as we will throw below if no other exception occurred prior to
        // calling parent and the parent
        // may throw ProviderNotFound even though a provider in the child already
        // handled the request
      }
      catch (AuthenticationException e) {
        lastException = parentException = e;
      }
    }
    // #4,如果認證通過,執行認證通過之後的操作
    if (result != null) {
      if (eraseCredentialsAfterAuthentication
          && (result instanceof CredentialsContainer)) {
        // Authentication is complete. Remove credentials and other secret data
        // from authentication
        ((CredentialsContainer) result).eraseCredentials();
      }

      // If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
      // This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
      if (parentResult == null) {
        eventPublisher.publishAuthenticationSuccess(result);
      }
      return result;
    }

    // Parent was null,or didn't authenticate (or throw an exception).
    // #5,如果認證不通過,必然有丟擲異常,否則表示沒有配置相應的認證部門(AuthenticationProvider)
    if (lastException == null) {
      lastException = new ProviderNotFoundException(messages.getMessage(
          "ProviderManager.providerNotFound",new Object[] { toTest.getName() },"No AuthenticationProvider found for {0}"));
    }

    // If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
    // This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
    if (parentException == null) {
      prepareException(lastException,authentication);
    }

    throw lastException;
  }
  ...
}

遍歷所有的認證部門(AuthenticationProvider),找到支援的認證部門進行認證認證部門進行認證認證通過則不再進行下一個認證部門的認證,否則丟擲的異常被捕獲,執行下一個認證部門(AuthenticationProvider)如果認證通過,執行認證通過之後的操作如果認證不通過,必然有丟擲異常,否則表示沒有配置相應的認證部門(AuthenticationProvider)

當使用到Spring Security OAuth2的時候,會看到另一個實現OAuth2AuthenticationManager

認證部門(AuthenticationProvider)

認證部門(AuthenticationProvider)負責實際的認證工作,與認證管理部門(ProvderManager)協同工作。也許其他的認證管理部門(AuthenticationManager)並不需要認證部門(AuthenticationProvider)的協作。

public interface AuthenticationProvider {
  // 進行認證
  Authentication authenticate(Authentication authentication)
      throws AuthenticationException;
  // 是否由該AuthenticationProvider進行認證
  boolean supports(Class<?> authentication);
}

該介面有很多的實現類,其中包含了RememberMeAuthenticationProvider(直接AuthenticationProvider)和DaoAuthenticationProvider(通過AbastractUserDetailsAuthenticationProvider簡介繼承)。這裡重點講講AbastractUserDetailsAuthenticationProviderDaoAuthenticationProvider

AbastractUserDetailsAuthenticationProvider

顧名思義,AbastractUserDetailsAuthenticationProvider是對UserDetails支援的Provider,其他的Provider,如RememberMeAuthenticationProvider就不需要用到UserDetails。該抽象類有兩個抽象方法需要實現類完成:

// 獲取 UserDetails
protected abstract UserDetails retrieveUser(String username,UsernamePasswordAuthenticationToken authentication)
    throws AuthenticationException;

protected abstract void additionalAuthenticationChecks(UserDetails userDetails,UsernamePasswordAuthenticationToken authentication)
    throws AuthenticationException;

retrieveUser()方法為校驗提供UserDetails。先看下UserDetails:

public interface UserDetails extends Serializable {
  
  Collection<? extends GrantedAuthority> getAuthorities();

  String getPassword();
  
  String getUsername();
  // 賬號是否過期
  boolean isAccountNonExpired();
  // 賬號是否被鎖
  boolean isAccountNonLocked();
  // 證書(password)是否過期
  boolean isCredentialsNonExpired();
  // 賬號是否可用
  boolean isEnabled();
}

AbastractUserDetailsAuthenticationProvider#authentication(Authentication)分為三步驗證:

  • preAuthenticationChecks.check(user);
  • additionalAuthenticationChecks(user,
  • (UsernamePasswordAuthenticationToken) authentication);
  • postAuthenticationChecks.check(user);

preAuthenticationChecks的預設實現為DefaultPreAuthenticationChecks,負責完成校驗:

  1. UserDetails#isAccountNonLocked()
  2. UserDetails#isEnabled()
  3. UserDetails#isAccountNonExpired()

postAuthenticationChecks的預設實現為DefaultPostAuthenticationChecks,負責完成校驗:

UserDetails#user.isCredentialsNonExpired()

additionalAuthenticationChecks需要由實現類完成。

校驗成功之後,AbstractUserDetailsAuthenticationProvider會建立並返回一個通過認證的Authentication

protected Authentication createSuccessAuthentication(Object principal,Authentication authentication,UserDetails user) {
  // Ensure we return the original credentials the user supplied,// so subsequent attempts are successful even with encoded passwords.
  // Also ensure we return the original getDetails(),so that future
  // authentication events after cache expiry contain the details
  UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
      principal,authentication.getCredentials(),authoritiesMapper.mapAuthorities(user.getAuthorities()));
  result.setDetails(authentication.getDetails());

  return result;
}

DaoAuthenticationProvider

如下為DaoAuthenticationProviderAbstractUserDetailsAuthenticationProvider抽象方法的實現。

// 檢查密碼是否正確
protected void additionalAuthenticationChecks(UserDetails userDetails,UsernamePasswordAuthenticationToken authentication)
    throws AuthenticationException {
  if (authentication.getCredentials() == null) {
    logger.debug("Authentication failed: no credentials provided");

    throw new BadCredentialsException(messages.getMessage(
        "AbstractUserDetailsAuthenticationProvider.badCredentials","Bad credentials"));
  }

  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"));
  }
}
// 通過資料室(UserDetailsService)獲取UserDetails物件
protected final UserDetails retrieveUser(String username,UsernamePasswordAuthenticationToken authentication)
    throws AuthenticationException {
  prepareTimingAttackProtection();
  try {
    UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
    if (loadedUser == null) {
      throw new InternalAuthenticationServiceException(
          "UserDetailsService returned null,which is an interface contract violation");
    }
    return loadedUser;
  }
  ...
}

在以上的程式碼中,需要提供UserDetailsServicePasswordEncoder例項。只要例項化這兩個類,並放入到Spring容器中即可。

資料部門(UserDetailsService)

UserDetailsService介面提供認證過程所需的UserDetails的類,如DaoAuthenticationProvider需要一個UserDetailsService例項。

public interface UserDetailsService {
  UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

Spring Security提供了兩個UserDetailsService的實現:InMemoryUserDetailsManagerJdbcUserDetailsManagerInMemoryUserDetailsManager為預設配置,從UserDetailsServiceAutoConfiguration的配置中可以看出。當然也不容易理解,基於資料庫的實現需要增加資料庫的配置,不適合做預設實現。這兩個類均為UserDetailsManager的實現類,UserDetailsManager定義了UserDetails的CRUD操作。InMemoryUserDetailsManager使用Map<String,MutableUserDetails>做儲存。

public interface UserDetailsManager extends UserDetailsService {
  void createUser(UserDetails user);

  void updateUser(UserDetails user);

  void deleteUser(String username);

  void changePassword(String oldPassword,String newPassword);

  boolean userExists(String username);
}

如果我們需要增加一個UserDetailsService,可以考慮實現UserDetailsService或者UserDetailsManager

增加一個認證流程

到這裡,我們已經知道Spring Security的流程了。從上面的內容可以知道,如要增加一個新的認證方式,只要增加一個[前臺(Filter) + 認證部門(AuthenticationProvider) + 資料室(UserDetailsService)]組合即可。事實上,資料室(UserDetailsService)不是必須的,可根據認證部門(AuthenticationProvider)需要實現。

詳解Spring Security認證流程

我會在另一篇文章中以手機號碼+驗證碼登入為例進行講解。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。