從源碼角度分析Shiro的驗(yàn)證過程

背景

  • 我們這個項(xiàng)目是前后端分離的架構(gòu)。由于前端在一次退出登錄時,存在同一用戶多次登錄的情況,導(dǎo)致退出登錄失敗!存在Redis服務(wù)器中的SessionId被刪除,也就不能再嘗試退出登錄了。
  • 但是更想不到的是,自此以后,不論賬號密碼對不對都報:"Realm [" com.cx.shiro.MyShiroRealm "] was unable to find account data for the " + "submitted AuthenticationToken [" + token + "]"。而且我在本地服務(wù)器測試賬號密碼都正確的情況下沒出現(xiàn)這個問題,那接下來就是通過服務(wù)器的日志進(jìn)行排查了。

排查思路

  1. 查看服務(wù)器日志,只發(fā)現(xiàn)執(zhí)行了一次查詢,然后再無下文。退出登錄出錯日志倒是很多,但是這并不影響我們排查登錄出錯的接口。因?yàn)槲野裄edis服務(wù)器的相關(guān)緩存清空了。
  2. 在本地服務(wù)器重現(xiàn)這個錯誤;
  3. 從這個錯誤調(diào)試從后往前查看一遍,再從前往后排查;
  4. 定位導(dǎo)致出錯的代碼。

具體解決

  1. 從錯誤中我們可以得知,該賬號不存在。所以我在我本地測試的時候輸入一個數(shù)據(jù)庫中并沒有的賬號,果然重現(xiàn)了這個錯誤。
  2. 接著啟動debug模式,來一步步進(jìn)行調(diào)試:
    先看一波shiro驗(yàn)證涉及的主要類圖:


    image.png

    image.png

    image.png

    再來一波方法調(diào)用圖:綠色的是接口,藍(lán)色的是類:


    image.png

首先在登錄操作的代碼上打上斷點(diǎn):

   @RequestMapping(value = "/login.do",method = RequestMethod.POST)
   @ResponseBody
   @ApiOperation(value = "登錄接口" ,notes = "根據(jù)用戶賬號密碼登錄" ,httpMethod = "POST")
    public ServerResponse Login(@Param("employeeId") String employeeId, @Param("password") String password) throws ParseException {
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(employeeId,password);
        usernamePasswordToken.setRememberMe(true);
        try{
            subject.login(usernamePasswordToken);  // 打斷點(diǎn)的位置
        }catch(UnknownAccountException e){
           return ServerResponse.createByErrorMessage(e.getMessage());
        }catch (IncorrectCredentialsException e){
            return ServerResponse.createByErrorMessage(e.getMessage());
        }catch (LockedAccountException e){
            return ServerResponse.createByErrorMessage(e.getMessage());
        }catch (AuthenticationException e){
            return ServerResponse.createByErrorMessage("賬戶驗(yàn)證失敗");
        }

subject.login()實(shí)際調(diào)用的是DelefatingSubject中的login()方法:

public void login(AuthenticationToken token) throws AuthenticationException {
        this.clearRunAsIdentitiesInternal();  // 如果session存在,則清除掉原有的session
        Subject subject = this.securityManager.login(this, token);//真正login的調(diào)用方法
        //以下代碼省略
        ...

接securityManaget.login()這個方法調(diào)用了DefaultSecurityManager.login(Subject subject, AuthenticationToken token)這個方法,接著往下看:

public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
        AuthenticationInfo info;
        try {
            info = this.authenticate(token); //實(shí)際進(jìn)行驗(yàn)證的方法
        } catch (AuthenticationException var7) {  // 拋出驗(yàn)證失敗的Exception
            AuthenticationException ae = var7;

            try {
                this.onFailedLogin(token, ae, subject);
            } catch (Exception var6) {
                if (log.isInfoEnabled()) {
                    log.info("onFailedLogin method threw an exception.  Logging and propagating original AuthenticationException.", var6);
                }
            }

            throw var7;
        }

        Subject loggedIn = this.createSubject(token, info, subject);
        this.onSuccessfulLogin(token, info, loggedIn);
        return loggedIn;
    }

那我們下一步就是查看這個真正進(jìn)行用戶驗(yàn)證的方法:
它在AuthenticatingSecurityManager.class中調(diào)用了 authenticate(AuthenticationToken token)方法

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

this.authenticator.authenticate(token)方法實(shí)際上是調(diào)用了 AbstractAuthenticator這個抽象類的authenticate方法。

public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
        if (token == null) {
            throw new IllegalArgumentException("Method argumet (authentication token) cannot be null.");
        } else {
            log.trace("Authentication attempt received for token [{}]", token);

            AuthenticationInfo info;
            try {
                info = this.doAuthenticate(token);//這里調(diào)用驗(yàn)證的方法
                if (info == null) {//這里可以知道info == null
                    //這個就是我們要找的錯誤
                    String msg = "No account information found for authentication token [" + token + "] by this " + "Authenticator instance.  Please check that it is configured correctly.";
                    throw new AuthenticationException(msg);
                }
            } catch (Throwable var8) {
                AuthenticationException ae = null;
                if (var8 instanceof AuthenticationException) {
                    ae = (AuthenticationException)var8;
                }

                if (ae == null) {
                    String msg = "Authentication failed for token submission [" + token + "].  Possible unexpected " + "error? (Typical or expected login exceptions should extend from AuthenticationException).";
                    ae = new AuthenticationException(msg, var8);
                }

                try {
                    this.notifyFailure(token, ae);
                } catch (Throwable var7) {
                    if (log.isWarnEnabled()) {
                        String msg = "Unable to send notification for failed authentication attempt - listener error?.  Please check your AuthenticationListener implementation(s).  Logging sending exception and propagating original AuthenticationException instead...";
                        log.warn(msg, var7);
                    }
                }

                throw ae;
            }

            log.debug("Authentication successful for token [{}].  Returned account [{}]", token, info);
            this.notifySuccess(token, info);
            return info;
        }
    }

this.doAuthenticate(token)調(diào)用的是ModularRealmAuthenticator.doAuthenticate(AuthenticationToken authenticationToken)方法

protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        this.assertRealmsConfigured();
        Collection<Realm> realms = this.getRealms(); //這個來查看我們
        return realms.size() == 1 ? this.doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken) : this.doMultiRealmAuthentication(realms, authenticationToken);
    }

這里如果你配置了多個Realm就調(diào)用

doMultiRealmAuthentication(realms, authenticationToken),

如果配置了一個Realm就調(diào)用

doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken)

這里我只說配置一個Realm的情況,多個Realm的情況有很多種,下次另開一篇來具體分析。doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken)是在ModularRealmAuthenticator中調(diào)用的

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);
        } else {
            //這個是驗(yàn)證方法,Realm是一個接口
            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);
            } else {
                return info;
            }
        }
    }

接著調(diào)用AuthenticatingRealm中的getAuthenticationInfo()方法

public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //先從緩存中嘗試拿到info
        AuthenticationInfo info = this.getCachedAuthenticationInfo(token);
        if (info == null) {
            //緩存中沒有再執(zhí)行驗(yàn)證
            info = this.doGetAuthenticationInfo(token);
            log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
            if (token != null && info != null) {
                this.cacheAuthenticationInfoIfPossible(token, info);
            }
        } else {
            log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
        }

AuthenticatingRealm中這是一個抽象方法,而我們自定義的Realm就是繼承該方法的,并且重寫了這個方法

 protected abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken var1) throws AuthenticationException;

最后我們看一下我們自定義的Realm里面重寫的這個方法

 //認(rèn)證
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //拿到賬號
        String employeeId = (String) authenticationToken.getPrincipal();
        //拿到密碼
        String password = new String((char[]) authenticationToken.getCredentials());
        //使用MD5進(jìn)行加密
        password = MD5Util.MD5Encode(password);
        LOGGER.error("password"+ password);
        //從數(shù)據(jù)庫中拿到對應(yīng)的員工的數(shù)據(jù)
        Employee employee = employeeService.selectEmployeeById(employeeId);
        //就是這個,錯誤的根源
        if((employee == null)||!employee.getPassword().equals(password)){
            return  null;
        }
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(employee,password,getName());
        //鹽值
      authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(PropertiesUtil.getProperty("password.salt")));
        return authenticationInfo;
    }

經(jīng)過我們上面的分析,我們知道返回的AuthenticationInfo是null。再看看我們自定義的Realm中的doGetAuthenticationInfo()方法,我們可以知道:

  1. employee找不到,為null;
  2. employee的password跟MD5加密后的密碼不同。
    我查看服務(wù)器日志,發(fā)現(xiàn)employee是存在的,不為null,那就只有通過MD5加密后的密碼和數(shù)據(jù)庫中的密碼不一致的情況了。
    所以我把密碼加密后的密碼和數(shù)據(jù)庫中的密碼打印出來,發(fā)現(xiàn)真的不一樣,而且很奇怪的就是鹽值讓我改了。什么?鹽值讓我改了?什么時候的事,我沒有!


    image.png

* 解決辦法:把鹽值改回來!把鹽值改回來!把鹽值改回來!給我氣的??!

但是為什么會出現(xiàn)本地測試沒問題,線上測試有問題呢!我覺得是:

  • 由于我設(shè)置session緩存的時間是一天,所以在這一天內(nèi),緩存的session不會消失。也就是我redis服務(wù)器在這一天內(nèi)一直都有這個session,但是線上服務(wù)器的session被刪了,沒錯,因?yàn)橥顺龅卿浧鋵?shí)是成功的!但是返回出錯,這個問題需要再解決一下。

總結(jié)

  • 由于自己的手賤,花了好久的時間才解決的這個bug。
  • shiro的源碼還是很容易懂的,建議新手可以讀一讀,有很多很好的設(shè)計(jì)。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容