AuthenticationManager 驗(yàn)證原理分析

UsernamePasswordAuthenticationFilter 源碼分析 中,最后在類UsernamePasswordAuthenticationFilter 的驗(yàn)證方法 attemptAuthentication() 會(huì)將用戶表單提交過來的用戶名和密碼封裝成對(duì)象委托類 AuthenticationManager 的驗(yàn)證方法 authenticate() 進(jìn)行身份驗(yàn)證蚊丐。

那么本文主要對(duì) AuthenticationManager 的驗(yàn)證方法 authenticate() 驗(yàn)證原理進(jìn)行源碼分析麦备。

AuthenticationManager 相關(guān)類圖

AuthenticationManager 驗(yàn)證過程涉及到的類和接口較多凛篙,先用一張類圖說明各個(gè)類和接口之間的關(guān)系,如下:

AuthenticationManager相關(guān)類圖.png
  • AuthenticationManager 為認(rèn)證管理接口類栏渺,其定義了認(rèn)證方法 authenticate()呛梆。
  • ProviderManager 為認(rèn)證管理類,實(shí)現(xiàn)了接口 AuthenticationManager 磕诊,并在認(rèn)證方法 authenticate() 中將身份認(rèn)證委托給具有認(rèn)證資格的 AuthenticationProvider 進(jìn)行身份認(rèn)證填物。
    ProviderManager中的成員變量 providers [List<AuthenticationProvider>] 存儲(chǔ)了一個(gè) AuthenticationProvider 類型的 List,和 spring security 配置文件相對(duì)應(yīng)秀仲,如下圖:
AuthenticationManager 對(duì)比圖
  • AuthenticationProvider 為認(rèn)證接口類壶笼,其定義了身份認(rèn)證方法 authenticate()神僵。
  • AbstractUserDetailsAuthenticationProvider 為認(rèn)證抽象類,實(shí)現(xiàn)了接口 AuthenticationProvider 定義的認(rèn)證方法 authenticate()覆劈。
    AbstractUserDetailsAuthenticationProvider 還定義了虛擬方法 retrieveUser() 用作查詢數(shù)據(jù)庫用戶信息保礼,以及虛擬方法 additionalAuthenticationChecks() 用作身份認(rèn)證。
  • DaoAuthenticationProvider 繼承自類 AbstractUserDetailsAuthenticationProvider责语,實(shí)現(xiàn)該類的方法 retrieveUser()additionalAuthenticationChecks()炮障。
    DaoAuthenticationProvider 中還具有成員變量 userDetailsService [UserDetailsService] 用作用戶信息查詢,以及成員變量 passwordEncoder [PasswordEncoder] 用作密碼的加密及驗(yàn)證坤候。
    DaoAuthenticationProviderspring security 配置文件相對(duì)應(yīng)胁赢,如下圖所示:
authenticationProvider 對(duì)比圖

流程分析

1、認(rèn)證的入口為 AuthenticationManagerauthenticate()方法白筹,鑒于 AuthenticationManager是接口類智末,因此分析它的實(shí)現(xiàn)類 ProviderManager谅摄,ProviderManagerauthenticate() 方法代碼如下:

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

-->1    for (AuthenticationProvider provider : getProviders()) {
            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);
                    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 = 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 = e;
            }
        }

        if (result != null) {
            if (eraseCredentialsAfterAuthentication
                    && (result instanceof CredentialsContainer)) {
                // Authentication is complete. Remove credentials and other secret data
                // from authentication
                ((CredentialsContainer) result).eraseCredentials();
            }

            eventPublisher.publishAuthenticationSuccess(result);
            return result;
        }

        // Parent was null, or didn't authenticate (or throw an exception).

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

        prepareException(lastException, authentication);

        throw lastException;
    }

其中:

  • -->1 處的 for 循環(huán)從該類的屬性 providers[List<AuthenticationProvider>] 中去取到支持該認(rèn)證的 AuthenticationProvider 來進(jìn)行認(rèn)證處理。
  • -->2 處代碼為使用支持該認(rèn)證的 AuthenticationProvider 對(duì)用戶身份進(jìn)行認(rèn)證系馆,使用該類進(jìn)行認(rèn)證如下文所示送漠。

2、在上文代碼的 -->2 處調(diào)用的代碼 result = provider.authenticate(authentication);由蘑,使用了 AuthenticationProviderauthenticate() 方法進(jìn)行認(rèn)證闽寡,接下來分析該方法,鑒于 AuthenticationProvider 是一個(gè)接口尼酿,因此分析它的實(shí)現(xiàn)類 AbstractUserDetailsAuthenticationProvider 的子類 DaoAuthenticationProvider 的認(rèn)證方法 authenticate()爷狈,代碼如下所示:

public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
                messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.onlySupports",
                        "Only UsernamePasswordAuthenticationToken is supported"));

        // Determine username
        String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
                : authentication.getName();

        boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(username);

        if (user == null) {
            cacheWasUsed = false;

            try {
-->1            user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            catch (UsernameNotFoundException notFound) {
                logger.debug("User '" + username + "' not found");

                if (hideUserNotFoundExceptions) {
                    throw new BadCredentialsException(messages.getMessage(
                            "AbstractUserDetailsAuthenticationProvider.badCredentials",
                            "Bad credentials"));
                }
                else {
                    throw notFound;
                }
            }

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

        try {
            preAuthenticationChecks.check(user);
-->2        additionalAuthenticationChecks(user,
                    (UsernamePasswordAuthenticationToken) authentication);
        }
        catch (AuthenticationException exception) {
            if (cacheWasUsed) {
                // There was a problem, so try again after checking
                // we're using latest data (i.e. not from the cache)
                cacheWasUsed = false;
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
                preAuthenticationChecks.check(user);
                additionalAuthenticationChecks(user,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            else {
                throw exception;
            }
        }

        postAuthenticationChecks.check(user);

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

        Object principalToReturn = user;

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

        return createSuccessAuthentication(principalToReturn, authentication, user);
    }

其中:

  • -->1 處的代碼表示調(diào)用方法 retrieveUser() 從數(shù)據(jù)庫中加載用戶信息。該方法代碼如下:
protected final UserDetails retrieveUser(String username,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        UserDetails loadedUser;

        try {
-->1.1  loadedUser = this.getUserDetailsService().loadUserByUsername(username);
        }
        catch (UsernameNotFoundException notFound) {
            if (authentication.getCredentials() != null) {
                String presentedPassword = authentication.getCredentials().toString();
                passwordEncoder.isPasswordValid(userNotFoundEncodedPassword,
                        presentedPassword, null);
            }
            throw notFound;
        }
        catch (Exception repositoryProblem) {
            throw new InternalAuthenticationServiceException(
                    repositoryProblem.getMessage(), repositoryProblem);
        }

        if (loadedUser == null) {
            throw new InternalAuthenticationServiceException(
                    "UserDetailsService returned null, which is an interface contract violation");
        }
        return loadedUser;
    }

在上述代碼中 -->1.1 處代碼的意思為:調(diào)用成員變量 userDetailsService 的方法 loadUserByUsername() 加載數(shù)據(jù)層中的用戶信息(是不是很熟悉)裳擎。

  • -->2 處的代碼為調(diào)用方法 additionalAuthenticationChecks() 密碼驗(yàn)證淆院,該方法代碼如下:
protected void additionalAuthenticationChecks(UserDetails userDetails,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        Object salt = null;

        if (this.saltSource != null) {
            salt = this.saltSource.getSalt(userDetails);
        }

        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();
->2.1 if (!passwordEncoder.isPasswordValid(userDetails.getPassword(),
                presentedPassword, salt)) {
            logger.debug("Authentication failed: password does not match stored value");

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

->2.1 出代碼調(diào)用了成員變量 passwordEncoder 的校驗(yàn)方法 isPasswordValid() 對(duì)用戶密碼進(jìn)行驗(yàn)證。(是不是很熟悉)

AuthenticationManager 流程圖.png

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末句惯,一起剝皮案震驚了整個(gè)濱河市土辩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌抢野,老刑警劉巖拷淘,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異指孤,居然都是意外死亡启涯,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門恃轩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來结洼,“玉大人,你說我怎么就攤上這事叉跛∷扇蹋” “怎么了?”我有些...
    開封第一講書人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵筷厘,是天一觀的道長鸣峭。 經(jīng)常有香客問我,道長酥艳,這世上最難降的妖魔是什么摊溶? 我笑而不...
    開封第一講書人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮充石,結(jié)果婚禮上莫换,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好拉岁,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開白布溃列。 她就那樣靜靜地躺著,像睡著了一般膛薛。 火紅的嫁衣襯著肌膚如雪听隐。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,741評(píng)論 1 289
  • 那天哄啄,我揣著相機(jī)與錄音雅任,去河邊找鬼。 笑死咨跌,一個(gè)胖子當(dāng)著我的面吹牛沪么,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播锌半,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼禽车,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了刊殉?” 一聲冷哼從身側(cè)響起殉摔,我...
    開封第一講書人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎记焊,沒想到半個(gè)月后逸月,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡遍膜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年碗硬,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瓢颅。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡恩尾,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出挽懦,到底是詐尸還是另有隱情翰意,我是刑警寧澤,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布巾兆,位于F島的核電站猎物,受9級(jí)特大地震影響虎囚,放射性物質(zhì)發(fā)生泄漏角塑。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一淘讥、第九天 我趴在偏房一處隱蔽的房頂上張望圃伶。 院中可真熱鬧,春花似錦、人聲如沸窒朋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽侥猩。三九已至榔至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間欺劳,已是汗流浹背唧取。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留划提,地道東北人枫弟。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像鹏往,于是被迫代替她去往敵國和親淡诗。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容