主頁 > 後端開發 > Spring Security認證流程分析(6)

Spring Security認證流程分析(6)

2022-08-04 11:35:51 後端開發

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的流程中了,具體認證程序就不再贅述了,

??以上就是整個認證流程,

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

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/500917.html

標籤:其他

上一篇:專案里的各種配置,你都了解嗎?

下一篇:02-專案實作讀寫分離

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more