Spring Security認證流程

前言

Spring Seuciry相關的內容看了實在是太多了,但總以爲仍是理解地不夠鞏固,仍是須要靠知識輸出作鞏固。 java

相關版本:spring

java: jdk 8  
spring-boot: 2.1.6.RELEASE

過濾器鏈和認證過程

Spring Security FilterChain

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

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

  1. Filter攔截請求,生成一個未認證的Authentication,交由AuthenticationManager進行認證;
  2. AuthenticationManager的默認實現ProviderManager會經過AuthenticationProviderAuthentication進行認證,其自己不作認證處理;
  3. 若是認證經過,則建立一個認證經過的Authentication返回;不然拋出異常,以表示認證不經過。

要理解這個過程,能夠從類UsernamePasswordAuthenticationFilterProviderManagerDaoAuthenticationProviderInMemoryUserDetailsManagerUserDetailsService實現類,由UserDetailsServiceAutoConfiguration默認配置提供)進行了解。只要建立一個含有spring-boot-starter-security的springboot項目,在適當地打上斷點接口看到這個流程。app

用認證部門進行講解

Authentication Flow to Company Flow) ide

請求到前臺以後,負責該請求的前臺會將請求的內容封裝爲一個Authentication對象交給認證管理部門認證管理部門僅管理認證部門,不作具體的認證操做,具體的操做由與該前臺相關的認證部門進行處理。固然,每一個認證部門須要判斷Authentication是否爲該部門負責,是則由該部門負責處理,不然交給下一個部門處理。認證部門認證成功以後會建立一個認證經過的Authentication返回。不然要麼拋出異常表示認證不經過,要麼交給下一個部門處理。spring-boot

若是須要新增認證類型,只要增長相應的前臺(Filter)和與該前臺(Filter)想對應的認證部門(AuthenticationProvider)就便可,固然也能夠增長一個與已有前臺對應的認證部門認證部門會經過前臺生成的Authentication來判斷該認證是否由該部門負責,於是也許提供一個二者相互認同的Authentication. 工具

認證部門須要人員資料時,則能夠從人員資料部門獲取。不一樣的系統有不一樣的人員資料部門,須要咱們提供該人員資料部門,不然將拿到空白檔案。固然,人員資料部門不必定是惟一的,認證部門能夠有本身的專屬資料部門post

上圖還能夠有以下的畫法:this

Authentication Flow to Company Flow

這個畫法可能會和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;
}

該接口的註釋中說明,必須按照以下的異常順序進行檢查和拋出:

  1. DisabledException:帳號不可用
  2. LockedException:帳號被鎖
  3. 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;
    }
    ...
}
  1. 遍歷全部的認證部門(AuthenticationProvider),找到支持的認證部門進行認證
  2. 認證部門進行認證
  3. 認證經過則再也不進行下一個認證部門的認證,不然拋出的異常被捕獲,執行下一個認證部門(AuthenticationProvider)
  4. 若是認證經過,執行認證經過以後的操做
  5. 若是認證不經過,必然有拋出異常,不然表示沒有配置相應的認證部門(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)分爲三步驗證:

  1. preAuthenticationChecks.check(user);
  2. additionalAuthenticationChecks(user,
    (UsernamePasswordAuthenticationToken) authentication);
  3. postAuthenticationChecks.check(user);

preAuthenticationChecks的默認實現爲DefaultPreAuthenticationChecks,負責完成校驗:

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

postAuthenticationChecks的默認實現爲DefaultPostAuthenticationChecks,負責完成校驗:

  1. 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)須要實現。

前臺 + 認證部門 + 資料室

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

相關文章
相關標籤/搜索