shiro認證流程分析

        在上一篇博文 springboot整合shiro入門 中,簡單介紹瞭如何使用shiro進行認證和授權,下面通過debug的方式(示例代碼還是上一篇博客使用的代碼),分析一下shiro是如何進行認證的:

      首先,回顧一下,處理登錄的方法:

    @PostMapping("/doLogin")
    @ResponseBody
    public String doLogin(String username,String password){
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        try {
            subject.login(token);
            return "登錄成功";
        }catch (UnknownAccountException|IncorrectCredentialsException e){
            e.printStackTrace();
            return "賬號或密碼錯誤";
        }catch (LockedAccountException e){
            e.printStackTrace();
            return "賬號已被鎖定,請聯繫管理員";
        }catch (AuthenticationException e){
            e.printStackTrace();
            return "未知異常,請聯繫管理員";
        }

    }

可以發現,登錄過程,調用的方法是:

subject.login(token);

這裏要debug的話,有兩種方式:

(1)通過一步步debug跟進去看看它做了什麼,這種方法比較通用,但是如果跟得很深的話,很容易暈。。。

(2)另外一種就是比較取巧的方法,因爲認證,肯定是要拿到用戶名和密碼進行比較的,而我們的用戶名和密碼都封裝到了          UsernamePasswordToken這個類中,那麼我們完全可以把斷點打在UsernamePasswordToken的獲取用戶名和密碼的方法上,然後看看方法調用棧即可知道其調用了哪些類的哪些方法了:

 

現在就可以啓動項目,訪問登錄接口進行登錄認證了,然後發現很順利的進入了獲取用戶名的斷點了:

那麼,我們從doLogin的subject.login(token)方法,一路看到UsernamePasswordToken獲取用戶名的方法,看看它做了什麼:

1. 首先是調用主體subject的login方法,把UsernamePasswordToken作爲參數傳入進去了:

subject.login(token);

2.進入到 DefaultSecurityManager 的login方法:

    public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
        AuthenticationInfo info;
        try {
            info = authenticate(token);
        } catch (AuthenticationException ae) {
            try {
                onFailedLogin(token, ae, subject);
            } catch (Exception e) {
                if (log.isInfoEnabled()) {
                    log.info("onFailedLogin method threw an " +
                            "exception.  Logging and propagating original AuthenticationException.", e);
                }
            }
            throw ae; //propagate
        }

        Subject loggedIn = createSubject(token, info, subject);

        onSuccessfulLogin(token, info, loggedIn);

        return loggedIn;
    }

並且停在了:

 info = authenticate(token);

其中,info就是要返回的認證信息,看一下DefaultSecurityManager的繼承關係,會發現它繼承了SessionsSecurityManager,而SessionsSecurityManager繼承了AuthenticatingSecurityManager,所以接下來它調用的是父類的認證方法

3.調用父類AuthenticatingSecurityManager的authenticate()方法:

    public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
        return this.authenticator.authenticate(token);
    }

發現它調用的是authenticator的authenticate()方法,那麼authenticator是什麼呢?它其實是ModularRealmAuthenticator,在構造方法裏面進行了初始化:

4. 調用ModularRealmAuthenticator的doAuthenticate()方法:

    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        assertRealmsConfigured();
        //獲取所有的realm
        Collection<Realm> realms = getRealms();
        if (realms.size() == 1) {
            return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
        } else {
            return doMultiRealmAuthentication(realms, authenticationToken);
        }
    }

它會先獲取所有的realm,然後判斷,如果只有個一個realm的話,就執行doSingleRealmAuthentication()方法,否則的話就執行doMultiRealmAuthentication()方法,這裏因爲只有一個realm,所以執行的doSingleRealmAuthentication

5.調用ModularRealmAuthenticator的doSingleRealmAuthentication()方法:

    protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
        if (!realm.supports(token)) {
            String msg = "Realm [" + realm + "] does not support authentication token [" +
                    token + "].  Please ensure that the appropriate Realm implementation is " +
                    "configured correctly or that the realm accepts AuthenticationTokens of this type.";
            throw new UnsupportedTokenException(msg);
        }
        AuthenticationInfo info = realm.getAuthenticationInfo(token);
        if (info == null) {
            String msg = "Realm [" + realm + "] was unable to find account data for the " +
                    "submitted AuthenticationToken [" + token + "].";
            throw new UnknownAccountException(msg);
        }
        return info;
    }

可以發現,它會調用:

AuthenticationInfo info = realm.getAuthenticationInfo(token);

這裏的realm,就是我們自定義的realm,而realm.getAuthenticationInfo(token);會先從緩存中取用戶信息,如果沒有,就會調用doGetAuthenticationInfo()方法:

info = doGetAuthenticationInfo(token);

而這個doGetAuthenticationInfo()就是我們自定義realm時重寫的方法。

6.執行自定義realm的doGetAuthenticationInfo方法,獲取用戶信息:

    //認證
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String username = (String) token.getPrincipal();
        //這裏模擬數據庫查詢用戶,根據用戶名查詢
        User dbUser = userService.getUserByUsername(username);
        if (dbUser == null){
            //賬號不存在
            throw new UnknownAccountException();
        }
        if (dbUser.getEnable()==0){
            //賬號被鎖定
            throw new LockedAccountException();
        }
        return new SimpleAuthenticationInfo(dbUser, dbUser.getPassword(), getName());
    }

到了這裏,就會根據前端傳入的用戶名到數據庫查詢用戶信息,封裝成SimpleAuthenticationInfo返回。

其實,現在只是驗證了該用戶是否存在,以及賬號是否被鎖定等,並沒有通過驗證,因爲密碼都還沒有比對。

所以放開獲取用戶名的斷點,看看接下來的密碼是如何比對的,現在到了獲取密碼的這個斷點:

 

回到AuthenticatingRealm的getAuthenticationInfo()方法,

    public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //從緩存中獲取用戶信息

        AuthenticationInfo info = getCachedAuthenticationInfo(token);
        if (info == null) {
            //如果緩存中沒有,則調用自定義realm的doGetAuthenticationInfo獲取
            info = doGetAuthenticationInfo(token);
            log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
            if (token != null && info != null) {
                cacheAuthenticationInfoIfPossible(token, info);
            }
        } else {
            log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
        }

        if (info != null) {
            //斷言密碼是匹配的
            assertCredentialsMatch(token, info);
        } else {
            log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}].  Returning null.", token);
        }

        return info;
    }

可以看到,有這麼一行代碼:

//斷言密碼是匹配的
 assertCredentialsMatch(token, info);

那麼,它肯定會在這裏進行密碼比對的,判斷密碼是否正確,我們跟下去看一下assertCredentialsMatch()方法:

    protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
        CredentialsMatcher cm = getCredentialsMatcher();
        if (cm != null) {
            if (!cm.doCredentialsMatch(token, info)) {
                //not successful - throw an exception to indicate this:
                String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials.";
                throw new IncorrectCredentialsException(msg);
            }
        } else {
            throw new AuthenticationException("A CredentialsMatcher must be configured in order to verify " +
                    "credentials during authentication.  If you do not wish for credentials to be examined, you " +
                    "can configure an " + AllowAllCredentialsMatcher.class.getName() + " instance.");
        }
    }

其中,cm是SimpleCredentialsMatcher類的實例,所以會調用它的doCredentialsMatch進行密碼匹配:

    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        //獲取前端傳入的密碼,也就是用戶輸入的密碼
        Object tokenCredentials = getCredentials(token);
        //獲取數據庫或者內存中存儲的密碼
        Object accountCredentials = getCredentials(info);
        //比對兩個密碼,判斷是否一致
        return equals(tokenCredentials, accountCredentials);
    }

       這裏思路也很清晰,就是獲取用戶輸入的密碼和存儲的密碼,然後比較兩個密碼是否相同。如果密碼相同,那麼身份認證就成功完成了。否則就會拋出異常。

 

到這裏,就完成了身份認證。

簡單總結一下認證流程:

1、調用Subject.login()方法
2、委託給DefaultSecurityManager的login方法
3、DefaultSecurityManager進一步委託給ModularRealmAuthenticator,調用其doAuthenticate()方法:
4、ModularRealmAuthenticator則會獲取到所有的realm,來獲取用戶信息
5、調用具體的realm的getAuthenticationInfo獲取用戶信息,緩存有則返回,否則通過doGetAuthenticationInfo來獲取用戶信息
6、獲取完用戶信息之後,則會調用SimpleCredentialsMatcher的doCredentialsMatch()方法進行密碼匹配,如果密碼相同,那麼身份認證就成功完成了,否則就會拋出異常。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章