zoukankan      html  css  js  c++  java
  • shiro认证流程源码分析--练气初期

    写在前面

    在上一篇文章当中,我们通过一个简单的例子,简单地认识了一下shiro。在这篇文章当中,我们将通过阅读源码的方式了解shiro的认证流程。

    建议大家边读文章边动手调试代码,这样效果会更好。

    认证异常分析

    shiro中的异常主要分为两类,一类是AuthenticationException认证异常,一类是AuthorizationException权限异常。分别对应http响应状态码中的401403

    认证异常AuthenticationException子类

    权限异常AuthorizationException子类

    当认证不通过时将根据具体情况抛出AuthenticationException的子类,当鉴权不通过时将会抛出AuthorizationException的子类。

    我们通过检验shiro是否抛出异常,从而判断登录对象是否通过认证、是否具备相关保护资源的访问权限。

    这也是我们在上一节的例子中,需要捕获相关异常的原因。

    接下来,我们通过阅读源码的方式来分析一下shiro框架的认证流程。

    认证流程分析

    /**认证器
     * @author 赖柄沣 bingfengdev@aliyun.com
     * @version 1.0
     * @date 2020/9/21 0:50
     */
    public class Authenticator {
    
        private DefaultSecurityManager securityManager;
    
        public Authenticator(){
            //1. 创建安全管理器
            this.securityManager = new DefaultSecurityManager();
    
            //2. 给安全管理器设置问题域
            //因为权限信息从ini文件中读取,所以是IniRealm
            this.securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
    
            //3. 注入安全管理器,并使用SecurityUtils全局安全工具类完成认证
            SecurityUtils.setSecurityManager(securityManager);
    
    
    
        }
    
        /**认证
         * @author 赖柄沣 bingfengdev@aliyun.com
         * @date 2020-09-23 16:22:11
         * @param username 用户名
         * @param password 密码
         * @return void
         * @version 1.0
         */
        public void authenticate(String username,String password){
            //4. 获取当前主题
            Subject subject = SecurityUtils.getSubject();
    
            //5.根据登录对象身份凭证信息创建登录令牌
            UsernamePasswordToken token = new UsernamePasswordToken(username,password);
    
            //6.认证
            //如果认证通过,则不抛出异常,否则抛出AuthenticationExceptixon异常子类
            //正式项目建议直接抛出,统一异常处理
            try {
                subject.login(token);
            }catch (IncorrectCredentialsException e) {
                e.printStackTrace();
            }catch (ConcurrentAccessException e){
                e.printStackTrace();
            }catch (UnknownAccountException e){
                e.printStackTrace();
            }catch (ExcessiveAttemptsException e){
                e.printStackTrace();
            }catch (ExpiredCredentialsException e){
                e.printStackTrace();
            }catch (LockedAccountException e){
                e.printStackTrace();
            }
    
        }
    
    
    }
    

    这是上一个例子当中的认证器的代码。 我们在上述代码的44行,shiro认证的入口处打个断点,以便跟踪其认证流程。

    然后在idea中以debug的形式启动程序。

    DelegatingSubject

    login()方法

    我们发现我们进入了DelegatingSubject.login方法当中;

    public class DelegatingSubject implements Subject {
        //省略了其他不影响理解的代码
          public void login(AuthenticationToken token) throws AuthenticationException {
             
            this.clearRunAsIdentitiesInternal();
              // 1. 真正做认证的还是securityManager对象
            Subject subject = this.securityManager.login(this, token);
            String host = null;
            PrincipalCollection principals;
            if (subject instanceof DelegatingSubject) {
                DelegatingSubject delegating = (DelegatingSubject)subject;
                principals = delegating.principals;
                host = delegating.host;
            } else {
                principals = subject.getPrincipals();
            }
    
            if (principals != null && !principals.isEmpty()) {
                this.principals = principals;
                this.authenticated = true;
                if (token instanceof HostAuthenticationToken) {
                    host = ((HostAuthenticationToken)token).getHost();
                }
    
                if (host != null) {
                    this.host = host;
                }
    
                Session session = subject.getSession(false);
                if (session != null) {
                    this.session = this.decorate(session);
                } else {
                    this.session = null;
                }
    
            } else {
                String msg = "Principals returned from securityManager.login( token ) returned a null or empty value.  This value must be non null and populated with one or more elements.";
                throw new IllegalStateException(msg);
            }
        }
     
    }
    

    从上面的源码中我们发现,虽然我们调用了Subject对象的认证方法,但是,真正的认证操作还是由安全管理器对象securityManager执行。

    DefaultSecurityManager

    login() 方法

    接着,我们进入到securityManager的login方法当中去。

    public class DefaultSecurityManager extends SessionsSecurityManager {
    
    
        //省略了其他无关代码
        public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
            AuthenticationInfo info;
            try {
                //调用认证方法
                info = this.authenticate(token);
            } catch (AuthenticationException var7) {
                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;
        }
    
    }
    

    AuthenticatingSecurityManager

    authenticate()方法

    当我们进入到authenticate方法中时,发现他是AuthenticatingSecurityManager的方法

    public abstract class AuthenticatingSecurityManager extends RealmSecurityManager {
    //省略了其他无关代码
    public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
        return this.authenticator.authenticate(token);
        }
    }
    

    AbstractAuthenticator

    authenticate()方法

    接着,他又调用了authenticator对象的authenticate方法

    public abstract class AbstractAuthenticator implements Authenticator, LogoutAware {
        //省略了其他无关方法
         public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
            if (token == null) {
                throw new IllegalArgumentException("Method argument (authentication token) cannot be null.");
            } else {
                log.trace("Authentication attempt received for token [{}]", token);
    
                AuthenticationInfo info;
                try {
                  
                    info = this.doAuthenticate(token);
                    if (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);
                        if (log.isWarnEnabled()) {
                            log.warn(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;
            }
        }
    }
    

    ModularRealmAuthenticator

    doAuthenticate()方法

    紧接着进入到了ModularRealmAuthenticator认证器对象的doAuthenticate方法

    public class ModularRealmAuthenticator extends AbstractAuthenticator {
        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);
        }
    }
    

    在这一步当中,在检验我们的Realms对象创建后,开始进入到doSingleRealmAuthentication方法当中进行认证操作

    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);
            } else {
                //获取认证信息
                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;
                }
            }
        }
    

    AuthenticatingRealm

    getAuthenticationInfo()方法

    在这一步当中开始根据我们传入的令牌获取认证信息

    public abstract class AuthenticatingRealm extends CachingRealm implements Initializable {
    
        public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            // 首先从缓存中获取
            AuthenticationInfo info = this.getCachedAuthenticationInfo(token);
            if (info == null) {
                //缓存中没有,则从持久化数据中获取
                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);
            }
    
            if (info != null) {
                this.assertCredentialsMatch(token, info);
            } else {
                log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}].  Returning null.", token);
            }
    
            return info;
        }
    }
    

    SimpleAccountRealm

    doGetAuthenticationInfo()方法

    从持久化数据源中获取登录对象信息

    public class SimpleAccountRealm extends AuthorizingRealm {
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken upToken = (UsernamePasswordToken)token;
        //根据用户名查询账户信息
        SimpleAccount account = this.getUser(upToken.getUsername());
        //如果查询到了账户信息
        if (account != null) {
            //开始判断账户状态
            if (account.isLocked()) {
                throw new LockedAccountException("Account [" + account + "] is locked.");
            }
    
            if (account.isCredentialsExpired()) {
                String msg = "The credentials for account [" + account + "] are expired";
                throw new ExpiredCredentialsException(msg);
            }
        }
    
        return account;
    }
    }
    

    在这里,便完成了对用户名的校验。

    AuthenticatingRealm

    接下来,我们获取到了账户信息并返回到了AuthenticatingRealm的getAuthenticationInfo方法。

    在这个方法中有如下几行代码,在第二行中,调用assertCredentialsMatch方法开始校验用户凭证

    if (info != null) {
        this.assertCredentialsMatch(token, info);
    } else {
        log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}].  Returning null.", token);
    }
    

    assertCredentialsMatch()方法

    protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
        //获取凭证匹配器对象
        CredentialsMatcher cm = this.getCredentialsMatcher();
        if (cm != null) {
            if (!cm.doCredentialsMatch(token, info)) {
                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.");
        }
    }
    

    在这里完成对用户凭证的校验。真正的比较逻辑则在SimpleCredentialsMatcher的equals方法中完成。里面还会区分加密和不加密的情况,具体请查看源码。

    分析到这一步我们可以发现,SimpleAccountRealm继承了AuthorizingRealm类实现doGetAuthenticationInfo方法完成账户信息查询并校验,并将结果返回给AuthorizingRealm。AuthorizingRealm帮SimpleAccountRealm完成对用户凭证的校验。

    那么,如果我们需要从数据库当中获取账户信息,应该怎么将账户信息传给shiro进行验证呢?这个问题留给大家思考一下,我将在下一篇文章当中为大家解答。

    写在最后

    在这篇文章当中,我们通过断点调试,阅读源码的方式弄清楚了shiro的认证流程。我们拆开他的层层封装,发现在SimpleAccountRealm对象中的doGetAuthenticationInfo方法中完成账户验证,在AuthenticatingRealm的assertCredentialsMatch完成对用户凭证的校验。

    在下一篇文章当中,我们将学习如何使用数据库信息完成认证和授权。

  • 相关阅读:
    maven:读取程序版本号的三种方案
    有无目标的人生差10倍!赶紧和娃把新年计划做起来
    都怎么了,其实早就知道,但是一直没有找到答案……
    python添加tab键功能
    电影观后感
    ipset批量配置iptables
    Oracle 触发器,事物
    Oracle PL/SQL高级应用 视图 同义词 序列
    Oracle PL/SQL高级应用 存储过程
    Oracle PL/SQL高级应用 游标
  • 原文地址:https://www.cnblogs.com/bingfengdev/p/13768829.html
Copyright © 2011-2022 走看看