Spring Security認證流程分析(6)

来源:https://www.cnblogs.com/liwenruo/archive/2022/08/04/16549567.html
-Advertisement-
Play Games

1.認證流程分析 Spring Security中預設的一套登錄流程是非常完善並且嚴謹的。但是項目需求非常多樣化, 很多時候,我們可能還需要對Spring Secinity登錄流程進行定製,定製的前提是開發者先深刻理解Spring Security登錄流程,然後在此基礎之上,完成對登錄流程的定製。本 ...


1.認證流程分析

  Spring Security中預設的一套登錄流程是非常完善並且嚴謹的。但是項目需求非常多樣化, 很多時候,我們可能還需要對Spring Secinity登錄流程進行定製,定製的前提是開發者先深刻理解Spring Security登錄流程,然後在此基礎之上,完成對登錄流程的定製。本文將從頭梳理 Spring Security登錄流程,並通過幾個常見的登錄定製案例,深刻地理解Spring Security登錄流程。

  本章涉及的主要知識點有:

  • 登錄流程分析。
  • 配置多個數據源。
  • 添加登錄驗證碼。

 1.1登錄流程分析

  要搞清楚Spring Security認證流程,我們得先認識與之相關的三個基本組件(Authentication 對象在前面文章種己經做過介紹,這裡不再贅述):AuthenticationManager、ProviderManager以及AuthenticationProvider,同時還要去瞭解接入認證功能的過濾器 AbstractAuthenticationProcessingFilter,這四個類搞明白了,基本上認證流程也就清楚了,下麵我們逐個分析一下。

  1.1.1 AuthenticationManager

  從名稱上可以看出,AuthenticationManager是一個認證管理器,它定義了 Spring Security 過濾器要如何執行認證操作。AuthenticationManager在認證成功後,會返回一個Authentication對象,這個Authentication對象會被設置到SecurityContextHolder中。如果開發者不想用Spring Security提供的一套認證機制,那麼也可以自定義認證流程,認證成功後,手動將Authentication 存入 SecurityContextHolder 中。

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

  從 AuthenticationManager 的源碼中可以看到,AuthenticationManager 對傳入的 Authentication對象進行身份認證,此時傳入的Authentication參數只有用戶名/密碼等簡單的屬性,如果認證成功,返回的Authentication的屬性會得到完全填充,包括用戶所具備的角色信息。AuthenticationManager是一個介面,它有著諸多的實現類,開發者也可以自定義 AuthenticationManager的實現類,不過在實際應用中,我們使用最多的是ProviderManager,在 Spring S ecurity 框架中,預設也是使用 ProviderManager。

1.1.2 AuthenticationProvider

  Spring Security支持多種不同的認證方式,不同的認證方式對應不同的身份 類型,AuthenticationProvider就是針對不同的身份類型執行具體的身份認證。例如,常見的 DaoAuthenticationProvider 用來支持用戶名/密碼登錄認證,RememberMeAuthenticationProvider 用來支持“記住我”的認證。

public interface AuthenticationProvider {
    Authentication authenticate(Authentication var1) throws AuthenticationException;
    boolean supports(Class<?> var1);
}
  • authenticate方法用來執行具體的身份憂證。
  • supports方法用來判斷當前AuthenticationProvider是否支持對應的身份類型。

  當使用用戶名/密碼的方式登錄時,對應的AuthenticationProvider實現類是 DaoAuthenticationProvider , 而 DaoAuthenticationProvider 繼承自 AbstractUserDetailsAuthenticationProvider並且沒有重寫authenticate方法,所以具體的認證邏輯在AbstractUserDetailsAuthenticationProvider 的 authenticate 方法中。我們就從 AbstractUserDetailsAuthenticationProvider開始看起:

  

查看代碼
 public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
    protected final Log logger = LogFactory.getLog(this.getClass());
    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
    private UserCache userCache = new NullUserCache();
    private boolean forcePrincipalAsString = false;
    protected boolean hideUserNotFoundExceptions = true;
    private UserDetailsChecker preAuthenticationChecks = new AbstractUserDetailsAuthenticationProvider.DefaultPreAuthenticationChecks();
    private UserDetailsChecker postAuthenticationChecks = new AbstractUserDetailsAuthenticationProvider.DefaultPostAuthenticationChecks();
    private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();

    public AbstractUserDetailsAuthenticationProvider() {
    }

    protected abstract void additionalAuthenticationChecks(UserDetails var1, UsernamePasswordAuthenticationToken var2) throws AuthenticationException;

    public final void afterPropertiesSet() throws Exception {
        Assert.notNull(this.userCache, "A user cache must be set");
        Assert.notNull(this.messages, "A message source must be set");
        this.doAfterPropertiesSet();
    }

    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
            return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
        });
        String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
        boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(username);
        if (user == null) {
            cacheWasUsed = false;

            try {
                user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
            } catch (UsernameNotFoundException var6) {
                this.logger.debug("User '" + username + "' not found");
                if (this.hideUserNotFoundExceptions) {
                    throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
                }

                throw var6;
            }

            Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
        }

        try {
            this.preAuthenticationChecks.check(user);
            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
        } catch (AuthenticationException var7) {
            if (!cacheWasUsed) {
                throw var7;
            }

            cacheWasUsed = false;
            user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
            this.preAuthenticationChecks.check(user);
            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
        }

        this.postAuthenticationChecks.check(user);
        if (!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }

        Object principalToReturn = user;
        if (this.forcePrincipalAsString) {
            principalToReturn = user.getUsername();
        }

        return this.createSuccessAuthentication(principalToReturn, authentication, user);
    }

    protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
        UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
        result.setDetails(authentication.getDetails());
        return result;
    }

    protected void doAfterPropertiesSet() throws Exception {
    }

    public UserCache getUserCache() {
        return this.userCache;
    }

    public boolean isForcePrincipalAsString() {
        return this.forcePrincipalAsString;
    }

    public boolean isHideUserNotFoundExceptions() {
        return this.hideUserNotFoundExceptions;
    }

    protected abstract UserDetails retrieveUser(String var1, UsernamePasswordAuthenticationToken var2) throws AuthenticationException;

    public void setForcePrincipalAsString(boolean forcePrincipalAsString) {
        this.forcePrincipalAsString = forcePrincipalAsString;
    }

    public void setHideUserNotFoundExceptions(boolean hideUserNotFoundExceptions) {
        this.hideUserNotFoundExceptions = hideUserNotFoundExceptions;
    }

    public void setMessageSource(MessageSource messageSource) {
        this.messages = new MessageSourceAccessor(messageSource);
    }

    public void setUserCache(UserCache userCache) {
        this.userCache = userCache;
    }

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

    protected UserDetailsChecker getPreAuthenticationChecks() {
        return this.preAuthenticationChecks;
    }

    public void setPreAuthenticationChecks(UserDetailsChecker preAuthenticationChecks) {
        this.preAuthenticationChecks = preAuthenticationChecks;
    }

    protected UserDetailsChecker getPostAuthenticationChecks() {
        return this.postAuthenticationChecks;
    }

    public void setPostAuthenticationChecks(UserDetailsChecker postAuthenticationChecks) {
        this.postAuthenticationChecks = postAuthenticationChecks;
    }

    public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
        this.authoritiesMapper = authoritiesMapper;
    }

    private class DefaultPostAuthenticationChecks implements UserDetailsChecker {
        private DefaultPostAuthenticationChecks() {
        }

        public void check(UserDetails user) {
            if (!user.isCredentialsNonExpired()) {
                AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account credentials have expired");
                throw new CredentialsExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.credentialsExpired", "User credentials have expired"));
            }
        }
    }

    private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
        private DefaultPreAuthenticationChecks() {
        }

        public void check(UserDetails user) {
            if (!user.isAccountNonLocked()) {
                AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account is locked");
                throw new LockedException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"));
            } else if (!user.isEnabled()) {
                AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account is disabled");
                throw new DisabledException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"));
            } else if (!user.isAccountNonExpired()) {
                AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account is expired");
                throw new AccountExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"));
            }
        }
    }
}
  • 一開始先聲明一個用戶緩存對象userCache,預設情況下沒有啟用緩存對象。
  • hideUserNotFoundExceptions表示是否隱藏用戶名查找失敗的異常,預設為true,為了確保系統安全,用戶在登錄失敗時只會給出一個模糊提示,例如“用戶名或密碼輸入錯誤“ 在Spring Security內部,如果用戶名查找失敗,則會拋出UsernameNotFoundException異常, 但是該異常會被自動隱藏,轉而通過一個BadCredentialsException異常來代替它,這樣,開發者在處理登錄失敗異常時,無論是用戶名輸入錯誤還是密碼輸入錯誤,收到的總是 BadCredentialsException,這樣做的一個好處是可以避免新手程式員將用戶名輸入錯誤和密碼輸入錯誤兩個異常分開提示。
  • forcePrincipalAsString表示是否強制將Principal對象當成字元串來處理,預設是falser,Authentication中的Principal屬性類型是一個Object,正常來說,通過Principal屬性可以獲取到當前登錄用戶對象(即UserDetails),但是如果forcePrincipalAsString設置為true,則 Authentication中的Principal屬性返回就是當前登錄用戶名,而不是用戶對象。
  • preAuthenticationChecks對象則是用於做用戶狀態檢査,在用戶認證過程中,需要檢驗用戶狀態是否正常,例如賬戶是否被鎖定、賬戶是否可用、賬戶是否過期等。
  • postAuthenticationChecks對象主要負責在密碼校驗成功後,檢査密碼是否過期。
  • additionalAuthenticationChecks是一個抽象方法,主要就是校驗密碼,具體的實現在 DaoAuthenticationProvider 中。
  • authenticate方法就是核心的校驗方法了。在方法中,首先從登錄數據中獲取用戶名, 然後根據用戶名去緩存中查詢用戶對象,如果査詢不到,則根據用戶名調用retrieveUser方法從資料庫中載入用戶;如果沒有載入到用戶,則拋出異常(用戶不存在異常會被隱藏)。拿到用戶對象之後,首先調用check方法進行用戶狀態檢査,然後調用 additionalAuthenticationChecks 方法進行密碼的校驗操作,最後調用 postAuthenticationChecks.check方法檢査密碼是否過期,當所有步驟都順利完成後,調用createSuccessAuthentication 方法創建一個認證後的 UsernamePasswordAuthenticationToken 對象並返回,認證後的對象中包含了認證主體、憑證以及角色等信息。

  這就是 AbstractUserDetailsAuthenticationProvider類的工作流程,有幾個抽象方法是在 DaoAuthenticationProvider 中實現的,我們再來看一下 DaoAuthenticationProvider中的定義:

  

查看代碼
 public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
    private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
    private PasswordEncoder passwordEncoder;
    private volatile String userNotFoundEncodedPassword;
    private UserDetailsService userDetailsService;
    private UserDetailsPasswordService userDetailsPasswordService;

    public DaoAuthenticationProvider() {
        this.setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
    }

    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            this.logger.debug("Authentication failed: no credentials provided");
            throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        } else {
            String presentedPassword = authentication.getCredentials().toString();
            if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
                this.logger.debug("Authentication failed: password does not match stored value");
                throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }
        }
    }

    protected void doAfterPropertiesSet() {
        Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");
    }

    protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        this.prepareTimingAttackProtection();

        try {
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
            } else {
                return loadedUser;
            }
        } catch (UsernameNotFoundException var4) {
            this.mitigateAgainstTimingAttack(authentication);
            throw var4;
        } catch (InternalAuthenticationServiceException var5) {
            throw var5;
        } catch (Exception var6) {
            throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
        }
    }

    protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
        boolean upgradeEncoding = this.userDetailsPasswordService != null && this.passwordEncoder.upgradeEncoding(user.getPassword());
        if (upgradeEncoding) {
            String presentedPassword = authentication.getCredentials().toString();
            String newPassword = this.passwordEncoder.encode(presentedPassword);
            user = this.userDetailsPasswordService.updatePassword(user, newPassword);
        }

        return super.createSuccessAuthentication(principal, authentication, user);
    }

    private void prepareTimingAttackProtection() {
        if (this.userNotFoundEncodedPassword == null) {
            this.userNotFoundEncodedPassword = this.passwordEncoder.encode("userNotFoundPassword");
        }

    }

    private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
        if (authentication.getCredentials() != null) {
            String presentedPassword = authentication.getCredentials().toString();
            this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
        }

    }

    public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
        Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
        this.passwordEncoder = passwordEncoder;
        this.userNotFoundEncodedPassword = null;
    }

    protected PasswordEncoder getPasswordEncoder() {
        return this.passwordEncoder;
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    protected UserDetailsService getUserDetailsService() {
        return this.userDetailsService;
    }

    public void setUserDetailsPasswordService(UserDetailsPasswordService userDetailsPasswordService) {
        this.userDetailsPasswordService = userDetailsPasswordService;
    }
}
  • 首先定義了 USER_NOT_FOUND_PASSWORD常量,這個是當用戶查找失敗時的預設密碼;passwordEncoder是一個密碼加密和比對工具,這個在後面會有專門的介紹,這裡先不做過多解釋;userNotFoundEncodedPassword變數則用來保存預設密碼加密後的值; userDetailsService是一個用戶查找工具,userDetailsService在前面己經講過,這裡不再贅述; userDetailsPasswordService則用來提供密碼修改服務。
  • DaoAuthenticationProvider的構造方法中,預設就會指定PasswordEncoder,當然開發者也可以通過set方法自定義PasswordEncoder
  • additionalAuthenticationchecks方法主要進行密碼校驗,該方法第一個參數userDetails 是從資料庫中查詢出來的用戶對象,第二個參數authentication則是登錄用戶輸入的參數。從這兩個參數中分別提取出來用戶密碼,然後調用passwordEncoder.matches方法進行密碼比對。
  •  retrieveUser方法則是獲取用戶對象的方法,具體做法就是調用 UserDetailsService#loadUserByUsername 方法去資料庫中查詢。
  • retrieveUser方法中,有一個值得關註的地方。在該方法一開始,首先會調用 prepareTimingAttackProtection 方法,該方法的作用是使用 PasswordEncoder 對常量 USER_NOT_FOUND_PASSWORD 進行加密,將加密結果保存在 userNotFoundEncodedPassword變數中。當根據用戶名查找用戶時如果拋出了 UsernameNotFoundException異常, 則調用mitigateAgainstTimingAttack方法進行密碼比對由有讀者會說,用戶都沒查找到,怎麼 比對密碼?需要註意,在調用mitigateAgainstTimingAttack方法進行密碼比對時,使用了 userNotFoundEncodedPassword變數作為預設密碼和登錄請求傳來的用戶密碼進行比對,這是 一個一開始就註定要失敗的密碼比對,那麼為什麼還要進行比對呢?這主要是為了避免旁道攻擊(Side-channel attack)。如果根據用戶名査找用戶失敗,就直接拋出異常而不進行密碼比對, 那麼黑客經過大量的測試,就會發現有的請求耗費時間明顯小於其他請求,那麼進而可以得出該請求的用戶名是一個不存在的用戶名(因為用戶名不存在,所以不需要密碼比對,進而節省時間),這樣就可以獲取到系統信息。為了避免這一問題,所以當用戶查找失敗時,也會調用 mitigateAgainstTimingAttack方法進行密碼比對,這樣就可以迷惑黑客。
  • createSuccessAuthentication方法則是在登錄成功後,創建一個全新的 UsernamePasswordAuthenticationToken對象,同時會判斷是否需要進行密碼升級,如果需要進行密碼升級,就會在該方法中進行加密方案升級。通過對 AbstractUserDetailsAuthenticationProvider DaoAuthenticationProvider 的講解,相信你己經很明白AuthenticationProvider中的認證邏輯了。

  在密碼學中,旁道攻擊(Side-channel attack )又稱側通道攻擊、邊通道攻擊。這種攻擊方式不是暴力破解或者是研究加密演算法的弱點。它是基於從密碼系統的物理實現中獲取信息, 比如時間、功率消耗、電磁泄漏等,這些信息可被利用於對系統的進一步破解。

  1.1.3 ProviderManager

  ProviderManager是AuthenticationManager的一個重要實現類,正在開始學習之前,我們先通過一幅圖來瞭解一下ProviderManager和AuthenticationProvider之間的關係,如圖3-1所示。

  

圖 3-1

  在Spring Security中,由於系統可能同時支持多種不同的認證方式,例如同時支持用戶名 /密碼認證、RememberMe認證、手機號碼動態認證等,而不同的認證方式對應了不同的 AuthenticationProvider,所以一個完整的認證流程可能由多個AuthenticationProvider來提供。

  多個AuthenticationProvider將組成一個列表,這個列表將由ProviderManager代理。換句話說,在 ProviderManager 中存在一個 AuthenticationProvider 列表,在 ProviderManager 中遍歷列表中的每一個AuthenticationProvider去執行身份認證,最終得到認證結果。

  ProviderManager 本身也可以再配置一個 AuthenticationManager 作為 parent,這樣當 ProviderManager認證失敗之後,就可以進入到parent中再次進行認證。理論上來說, ProviderManager的parent可以是任意類型的 AuthenticationManager,但是通常都是由 ProviderManager 來扮演 parent 的角色,也就是 ProviderManager 是 ProviderManager 的 parent。

  ProviderManager本身也可以有多個,多個ProviderManager共用同一個parent,當存在多個過濾器鏈的時候非常有用。當存在多個過濾器鏈時,不同的路徑可能對應不同的認證方式, 但是不同路徑可能又會同時存在一些共有的認證方式,這些共有的認證方式可以在parent中統 一處理。

  根據上面的介紹,圖 3-2是ProviderManager和AuthenticationProvider關係圖。

  

圖 3-2

  我們重點看一下ProviderManager中的authenticate方法:

 

查看代碼
 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();
        Iterator var8 = this.getProviders().iterator();

        while(var8.hasNext()) {
            AuthenticationProvider provider = (AuthenticationProvider)var8.next();
            if (provider.supports(toTest)) {
                if (debug) {
                    logger.debug("Authentication attempt using " + provider.getClass().getName());
                }

                try {
                    result = provider.authenticate(authentication);
                    if (result != null) {
                        this.copyDetails(authentication, result);
                        break;
                    }
                } catch (InternalAuthenticationServiceException | AccountStatusException var13) {
                    this.prepareException(var13, authentication);
                    throw var13;
                } catch (AuthenticationException var14) {
                    lastException = var14;
                }
            }
        }

        if (result == null && this.parent != null) {
            try {
                result = parentResult = this.parent.authenticate(authentication);
            } catch (ProviderNotFoundException var11) {
            } catch (AuthenticationException var12) {
                parentException = var12;
                lastException = var12;
            }
        }

        if (result != null) {
            if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
                ((CredentialsContainer)result).eraseCredentials();
            }

            if (parentResult == null) {
                this.eventPublisher.publishAuthenticationSuccess(result);
            }

            return result;
        } else {
            if (lastException == null) {
                lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
            }

            if (parentException == null) {
                this.prepareException((AuthenticationException)lastException, authentication);
            }

            throw lastException;
        }
    }

   

這段源碼的邏輯還是非常清晰的,我們分析一下:

  • 首先獲取authentication對象的類型。
  • 分別定義當前認證過程拋出的異常、parent中認證時拋出的異常、當前認證結果以及parent中認證結果對應的變數。
  • getProviders 方法用來獲取當前 ProviderManager 所代理的所有 AuthenticationProvider 對象,遍歷這些AuthenticationProvider對象進行身份認證。
  • 判斷當前AuthenticationProvider是否支持當前Authentication對象,要是不支持,則繼續處理列表中的下一個AuthenticationProvider對象
  • 調用provider.authenticate方法進行身份認證,如果認證成功,返回認證後的 Authentication對象,同時調用copyDetails方法給Authentication對象的details屬性賦值,由於可能是多個AuthenticationProvider執行認證操作,所以如果拋出異常則通過lastException 變數來記錄。
  • 在for迴圈執行完成後,如果result還是沒有值,說明所有的AuthenticationProvider 都認證失敗,此時如果parent不為空,則調用parentauthenticate方法進行認證。
  • 接下來,如果result不為空,就將result中的憑證擦除,防止泄漏,如果使用了用戶名/密碼的方式登錄,那麼所謂的擦除實際上就是將密碼欄位設置為null,同時將登錄成功的事件發佈出去(發佈登錄成功事件需要parentResultnull。如果parentResult不為null,表示在parent中已經認證成功了,認證成功的事件也己經在parent中發佈出去了這樣會導致認證成功的事件重覆發佈)。如果用戶認證成功,此時就將result返回,後面的代碼也就不再執行了。
  • 如果前面沒能返回result,說明認證失敗。如果lastExceptionnull,說明parentnull或者沒有認證亦或者認證失敗了但是沒有拋出異常,此時構造ProviderNotFoundException 異常賦值給lastException。
  • 如果parentExceptionnull,發佈認證失敗事件(如果parentException不為null, 則說明認證失敗事件已經發佈過了)。
  • 最後拋出lastException異常。

  這就是ProviderManagerauthenticate方法的身份認證邏輯,其他方法的源碼要相對簡單很多,在這裡就不一一解釋了,

  現在,大家已經熟悉了 AuthenticationAuthenticationManagerAuthenticationProvider 以 及ProviderManager的工作原理了,接下來的問題就是這些組件如何跟登錄關聯起來?這就涉及一個重要的過濾器----------------------- AbstractAuthenticationProcessingFilter

  1.1.4 AbstractAuthenticationProcessingFilter

  作為 Spring Security 過濾器鏈中的一環,AbstractAuthenticationProcessingFilter可以用來處理任何提交給它的身份認證,圖3-3描述了 AbstractAuthenticationProcessingFilter的工作流程:

  

圖 3-3

  圖中顯示的流程是一個通用的架構。

  AbstractAuthenticationProcessingFilter作為一個抽象類,如果使用用戶名/密碼的方式登錄, 那麼它對應的實現類是 UsernamePasswordAuthenticationFilter;構造出來的 Authentication 對象則是 UsernamePasswordAuthenticationToken。至於 AuthenticationManager,前面已經說過,一 般情況下它的實現類就是ProviderManager,這裡在ProviderManager中進行認證,認證成功就會進入認證成功的回調,否則進入認證失敗的回調。因此,我們可以對上面的流程圖再做進一 步細化,如圖3-4所示。

  

圖 3-4

  前面第2章中所涉及的認證流程基本上就是這樣,我們來大致梳理一下:

  • 當用戶提交登錄請求時,UsernamePasswordAuthenticationFilter會從當前請求 HttpServletRequest中提取出登錄用戶名/密碼,然後創建出一個 UsernamePasswordAuthenticationToken 對象。
  • UsernamePasswordAuthenticationToken 對象將被傳入 ProviderManager 中進行具體的認證操作。
  • 如果認證失敗,則SecurityContextHolder中相關信息將被清除,登錄失敗回調也會被調用,
  • 如果認證成功,則會進行登錄信息存儲、Session併發處理、登錄成功事件發佈以及登錄成功方法回調等操作。

  這是認證的一個大致流程。接下來我們結合 AbstractAuthenticationProcessingFilterUsernamePasswordAuthenticationFilter的源碼來看一下。

  先來看 AbstractAuthenticationProcessingFilter源碼(部分核心代碼):

  

查看代碼
 public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
		implements ApplicationEventPublisherAware, MessageSourceAware {
	protected ApplicationEventPublisher eventPublisher;
	protected AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
	private AuthenticationManager authenticationManager;
	protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
	private RememberMeServices rememberMeServices = new NullRememberMeServices();
	private RequestMatcher requiresAuthenticationRequestMatcher;
	private boolean continueChainBeforeSuccessfulAuthentication = false;
	private SessionAuthenticationStrategy sessionStrategy = new NullAuthenticatedSessionStrategy();
	private boolean allowSessionCreation = true;
	private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
	private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
	protected AbstractAuthenticationProcessingFilter(String defaultFilterProcessesUrl) {
		setFilterProcessesUrl(defaultFilterProcessesUrl);
	}

	protected AbstractAuthenticationProcessingFilter(
			RequestMatcher requiresAuthenticationRequestMatcher) {
		Assert.notNull(requiresAuthenticationRequestMatcher,
				"requiresAuthenticationRequestMatcher cannot be null");
		this.requiresAuthenticationRequestMatcher = requiresAuthenticationRequestMatcher;
	}

	@Override
	public void afterPropertiesSet() {
		Assert.notNull(authenticationManager, "authenticationManager must be specified");
	}

	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;
		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);
			return;
		}
		if (logger.isDebugEnabled()) {
			logger.debug("Request is to process authentication");
		}
		Authentication authResult;
		try {
			authResult = attemptAuthentication(request, response);
			if (authResult == null) {
				return;
			}
			sessionStrategy.onAuthentication(authResult, request, response);
		}
		catch (InternalAuthenticationServiceException failed) {
			logger.error(
					"An internal error occurred while trying to authenticate the user.",
					failed);
			unsuccessfulAuthentication(request, response, failed);
			return;
		}
		catch (AuthenticationException failed) {
			unsuccessfulAuthentication(request, response, failed);
			return;
		}
		if (continueChainBeforeSuccessfulAuthentication) {
			chain.doFilter(request, response);
		}
		successfulAuthentication(request, response, chain, authResult);
	}

	protected boolean requiresAuthentication(HttpServletRequest request,
			HttpServletResponse response) {
		return requiresAuthenticationRequestMatcher.matches(request);
	}

	public abstract Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException, IOException,
			ServletException;
	protected void successfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, Authentication authResult)
			throws IOException, ServletException {
		if (logger.isDebugEnabled()) {
			logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
					+ authResult);
		}
		SecurityContextHolder.getContext().setAuthentication(authResult);
		rememberMeServices.loginSuccess(request, response, authResult);
		if (this.eventPublisher != null) {
			eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
					authResult, this.getClass()));
		}
		successHandler.onAuthenticationSuccess(request, response, authResult);
	}

	protected void unsuccessfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, AuthenticationException failed)
			throws IOException, ServletException {
		SecurityContextHolder.clearContext();

		if (logger.isDebugEnabled()) {
			logger.debug("Authentication request failed: " + failed.toString(), failed);
			logger.debug("Updated SecurityContextHolder to contain null Authentication");
			logger.debug("Delegating to authentication failure handler " + failureHandler);
		}

		rememberMeServices.loginFail(request, response);

		failureHandler.onAuthenticationFailure(request, response, failed);
	}
}
  • 首先通過requiresAuthentication方法來判斷當前請求是不是登錄認證請求,如果是認證請求,就執行接下來的認證代碼;如果不是認證請求,則直接繼續走剩餘的過濾器即可。
  • 調用attemptAuthentication方法來獲取一個經過認證後的Authentication對象, attemptAuthentication方法是一個抽象方法,具體實現在它的子類 UsernamePasswordAuthenticationFilter 中。
  • 認證成功後,通過sessionStrategy.onAuthentication方法來處理session併發問題。
  • continueChainBeforeSuccessfulAuthentication變數用來判斷請求是否還需要繼續向下走,預設情況下該參數的值為false,即認證成功後,後續的過濾器將不再執行了。
  • unsuccessfulAuthentication方法用來處理認證失敗事宜,主要做了三件事:①從 SecurityContextHolder中清除數據;②清除Cookie等信息;③調用認證失敗的回調方法。
  • successfulAuthentication方法主要用來處理認證成功事宜,主要做了四件事:①向 SecurityContextHolder中存入用戶信息;②處理Cookie;③發佈認證成功事件,這個事件類型 InteractiveAuthenticationSuccessEvent,表示通過一些自動交互的方式認證成功,例如通過 RememberMe的方式登錄;④調用認證成功的回調方法。

  這就是 AbstractAuthenticationProcessingFilter大致上所做的事情,還有一個抽象方法 attemptAuthentication 是在它的繼承類 UsernamePasswordAuthenticationFilter中實現的,接下來我們來看—下UsernamePasswordAuthenticationFilter類:

查看代碼
public class UsernamePasswordAuthenticationFilter extends
		AbstractAuthenticationProcessingFilter {
	public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
	public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
	private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
	private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
	private boolean postOnly = true;

	public UsernamePasswordAuthenticationFilter() {
		super(new AntPathRequestMatcher("/login", "POST"));
	}

	public Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException {
		if (postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException(
					"Authentication method not supported: " + request.getMethod());
		}
		String username = obtainUsername(request);
		String password = obtainPassword(request);
		if (username == null) {
			username = "";
		}
		if (password == null) {
			password = "";
		}
		username = username.trim();
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);
		setDetails(request, authRequest);
		return this.getAuthenticationManager().authenticate(authRequest);
	}
    
    @Nullable
	protected String obtainPassword(HttpServletRequest request) {
		return request.getParameter(passwordParameter);
	}

	@Nullable
	protected String obtainUsername(HttpServletRequest request) {
		return request.getParameter(usernameParameter);
	}

}
  • 首先聲明瞭預設情況下登錄表單的用戶名欄位和密碼欄位,用戶名欄位的key預設是username,密碼欄位的key預設是password。當然,這兩個欄位也可以自定義,自定義的方式就是我們在 SecurityConfig 中配置的  .usernameParameter("uname")和  .passwordParameter("passwd")(參考前幾節)
  • UsernamePasswordAuthenticationFilter過濾器構建的時候,指定了當前過濾器只用來處理登錄請求,預設的登錄請求是/login,當然開發者也可以自行配置。
  • 接下來就是最重要的attemptAuthentication方法了,在該方法中,首先確認請求是 post類型;然後通過obtainUsernameobtainPassword方法分別從請求中提取出用戶名和密碼, 具體的提取過程就是調用request.getParameter方法;拿到登錄請求傳來的用戶名/密碼之後, 構造出一個 authRequest,然後調用 getAuthenticationManager().authenticate 方法進行認證,這就進入到我們前面所說的ProviderManager的流程中了,具體認證過程就不再贅述了。

  以上就是整個認證流程。

  搞懂了認證流程,那麼接下來如果想要自定義一些認證方式,就會非常容易了,比如定義多個數據源、添加登錄校驗碼等。下麵,我們將通過兩個案例,來活學活用上面所講的認證流程。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 最近在研究所做網路終端測試的項目,包括一些嵌入式和底層數據幀的封裝調用。之前很少接觸對二進位原始數據的處理與封裝,所以在此進行整理。 ...
  • 跨域指的是瀏覽器不能執行其他網站的腳本。它是由瀏覽器的同源策略造成的,是瀏覽器對JavaScript施加的安全限制。在做前後端分離項目的時候就需要解決此問題。 ...
  • 在眾多編程語言中,Python的社區生態是其中的佼佼者之一。幾乎所有的技術痛點,例如優化代碼提升速度,在社區內都有很多成功的解決方案。本文分享的就是一份可以令 Python 變快的工具清單,值得瞭解下。 一、序言 這篇文章會提供一些優化代碼的工具。會讓代碼變得更簡潔,或者更迅速。 當然這些並不能代替 ...
  • 動態數組底層是如何實現的 引言: 提到數組,大部分腦海裡一下子想到了一堆東西 int long short byte float double boolean char String 沒錯,他們也可以定義成數組 但是,上面都是靜態的 不過,咱們今天學習的可是動態的(ArrayList 數組) 好接下 ...
  • Java 的集合體系 Java集合可分為兩大體系:Collection 和 Map 1.常見的Java集合如下: Collection介面:單列數據,定義了存取一組對象的方法的集合 List:元素有序(指的是存取時,與存放順序保持一致)、可重覆的集合 Set:元素無序、不可重覆的集合 Map介面:雙 ...
  • 1.配置多個數據源 多個數據源是指在同一個系統中,用戶數據來自不同的表,在認證時,如果第一張表沒有查找到用戶,那就去第二張表中査詢,依次類推。 看了前面的分析,要實現這個需求就很容易了,認證要經過AuthenticationProvider,每一 個 AuthenticationProvider 中 ...
  • 第一步 下載新版idea安裝包idea2022.x。 下載方式(推薦):訪問idea官網選擇idea2022旗艦版本進行下載即可,不要選擇community版本哦(community版本是社區版,它是免費的,不用激活的,但是功能少於旗艦版)。當然,如果社區版功能滿足你的需求,選它即可。 安裝 下載好 ...
  • 一、介紹 instanceof是在多態中引出的,因為在多態發生時,子類只能調用父類中的方法(編譯時類型的方法),而子類自己獨有的方法(運行時類型的方法)無法調用,如果強制調用的話就需要向下轉型,語法和基本類型的強制類型轉換一樣;但是向下轉型具有一定的風險,很有可能無法成功轉化,為了判斷能否成功轉化, ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...