SHIRO源码解读——Authenticator身份认证

作者: PioneerYi | 来源:发表于2019-07-28 10:40 被阅读10次

    身份认证,即在应用中证明他就是他本人。一般提供如他们的身份ID一些标识信息来 表明他就是他本人,如提供身份证,用户名/密码来证明。在 shiro 中,用户需要提供 principals (身份)和 credentials(证明)给 shiro,从而应用能 验证用户身份。本文重点关注利用shiro进行身份认证时,shiro的内部工作流程。

    一、使用shiro身份认证demo

    使用shiro进行身份认证的最简单的demo如下:

    public void testHelloworld() {
        //1、获取 SecurityManager 工厂,此处使用 Ini 配置文件初始化SecurityManager 
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        //2、得到 SecurityManager 实例 并绑定给 SecurityUtils 
        SecurityManager securityManager = factory.getInstance();
        SecurityUtils.setSecurityManager(securityManager);
        //3、得到 Subject 及创建用户名/密码身份验证 Token(即用户身份/凭证) 
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken("zhang", "123");
        try {
            //4、登录,即身份验证
            subject.login(token);
        } catch (AuthenticationException e) { 
            //5、身份验证失败
        }
        Assert.assertEquals(true, subject.isAuthenticated()); //断言用户已经登录
        //6、退出
        subject.logout();
    }
    

    从如上代码可总结出身份验证的步骤:
    1、收集用户身份/凭证,即如用户名/密码;
    2、调用 Subject.login 进行登录,如果失败将得到相应的 AuthenticationException 异常,根 据异常提示用户错误信息;否则登录成功;
    3、最后调用 Subject.logout 进行退出操作。

    那么SHIRO内部流程是怎样的了,在创建完SecurityManager后,接下来的步骤内部做了什么工作了?

    二、创建Subject

    创建完SecurityManager后,首先将SecurityManager绑定给了securityUtils,然后是获取Subject。

    public static Subject getSubject() {
        Subject subject = ThreadContext.getSubject();
        if (subject == null) {
            subject = (new Subject.Builder()).buildSubject();
            ThreadContext.bind(subject);
        }
        return subject;
    }
    

    这步首先构造Subject.Builder,Subject采用的Builder模式,然后调用Builder的buildeSubject()方法创建一个Subject对象,具体过程如下:

    public Builder() {
       this(SecurityUtils.getSecurityManager());
    }
    
    public Builder(SecurityManager securityManager) {
        this.securityManager = securityManager;
        this.subjectContext = newSubjectContextInstance();
        this.subjectContext. setSecurityManager(securityManager);
    }
    

    其中SubjectContext为新建一个默认的DefaultSubjectContext();

    protected SubjectContext newSubjectContextInstance() {
        return new DefaultSubjectContext();
    }
    

    创建好Builder后,调用builderSubject方法,其内部是委托给SecurityManager创建Subject的。

    public Subject buildSubject() {
        return this.securityManager.createSubject(this.subjectContext);
    }
    

    SecurityManager创建过程是:首先创建一个默认的DefaultSecurityManager,然后根据配置再更新其内部属性和成员,DefaultSecurityManager中创建Subject方法如下:

    public Subject createSubject(SubjectContext subjectContext) {
        SubjectContext context = copy(subjectContext);
        context = ensureSecurityManager(context);
        context = resolveSession(context);
        context = resolvePrincipals(context);
        Subject subject = doCreateSubject(context);
        save(subject);
        return subject;
    }
    

    其中创建Subject的方法为Subject subject = doCreateSubject(context),方法定义如下:

    protected Subject doCreateSubject(SubjectContext context) {
        return getSubjectFactory().createSubject(context);
    }
    

    方法内部使用的是SubjectFactory创建的,具体使用的是DefaultSubjectFactory,然后调用其createSubject方法,方法定义如下:

    public Subject createSubject(SubjectContext context) {
        SecurityManager securityManager = context.resolveSecurityManager();
        Session session = context.resolveSession();
        boolean sessionCreationEnabled = context.isSessionCreationEnabled();
        PrincipalCollection principals = context.resolvePrincipals();
        boolean authenticated = context.resolveAuthenticated();
        String host = context.resolveHost();
        return new DelegatingSubject(principals, authenticated, host, session, sessionCreationEnabled, securityManager);
    }
    

    从上述方法可以看到,方法返回的是一个DelegatingSubject,DelegatingSubject是Subject子类,其源码注释如下:

    /**
     * Implementation of the {@code Subject} interface that delegates
     * method calls to an underlying {@link org.apache.shiro.mgt.SecurityManager SecurityManager} instance for security checks.
     * It is essentially a {@code SecurityManager} proxy.
     * <p/>
     **/
    

    至此,Subject创建完毕!

    三、身份认证

    首先看看Shiro核心逻辑的类图:


    Shiro核心逻辑类图

    后面的分析均是基于此类图,下面进行详细分析。

    代码subject.login(token)即进行身份认证,首先看看login方法:

    public void login(AuthenticationToken token) throws AuthenticationException {
        Subject subject = securityManager.login(this, token);
        PrincipalCollection principals;
        String host = null;
        if (subject instanceof DelegatingSubject) {
            DelegatingSubject delegating = (DelegatingSubject) subject;
            principals = delegating.principals;
            host = delegating.host;
        } else {
            principals = subject.getPrincipals();
        }
        ...
    }
    

    在上面代码的第三行:Subject subject = securityManager.login(this, token); 注意到其调用了SecurityManager的login方法,login方法最终的实现在类DefaultSecurityManager中,方法如下:

    public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
        AuthenticationInfo info;
        try {
            info = authenticate(token);
        } catch (AuthenticationException ae) {
            onFailedLogin(token, ae, subject);
        }
        Subject loggedIn = createSubject(token, info, subject);
        onSuccessfulLogin(token, info, loggedIn);
        return loggedIn;
    }
    

    身份校验authenticate方法是在DefaultSecurityManager的父类AuthenticatingSecurityManager定义的,AuthenticatingSecurityManager.authenticate方法如下:

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

    其中委托给Authenticator,AbstractAuthenticator中authenticate(token)方法定义如下:

    public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
        AuthenticationInfo info;
        try {
            info = doAuthenticate(token);
            }
        } catch (Throwable t) {
            notifyFailure(token, exception);
        }
        notifySuccess(token, info);
        return info;
    }
    

    真正身份校验逻辑是在ModularRealmAuthenticator中,ModularRealmAuthticator的doAuthenticate(token)方法如下:

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

    当只有一个Realm时,就执行doSingleRealmAuthentication,当有多个Realm时,就执行doMultiRealmAuthentication。我们看看doSingleRealmAuthentication中干了什么:

    protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
        AuthenticationInfo info = realm.getAuthenticationInfo(token);
        return info;
    }
    

    上面代码:AuthenticationInfo info = realm.getAuthenticationInfo(token); realm为Realm接口,实际上调用的是其实现类AuthenticatingRealm中的getAuthenticationInfo方法,方法如下:

    public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        AuthenticationInfo info = this.getCachedAuthenticationInfo(token);
        if (info == null) {
            info = this.doGetAuthenticationInfo(token);
            if (token != null && info != null) {
                this.cacheAuthenticationInfoIfPossible(token, info);
            }
        }
        ...
        return info;
    }
    

    其中的this.doGetAuthenticationInfo(token)是一个抽象方法,真正的实现在我们自己定义的Realm。

    三、总结

    身份认证流程

    身份认证流程如下:
    1)如果没有创建Subject,创建一个Subject;
    2)调用 Subject.login(token)进行登录,其会自动委托给 Security Manager,调用之前必须通过 SecurityUtils. setSecurityManager()设置;
    3)SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份验证;
    4)Authenticator 才是真正的身份验证者,Shiro API 中核心的身份认证入口点,此处可以自定义插入自己的实现;
    5)Authenticator 可能会委托给相应的 AuthenticationStrategy 进行多 Realm 身份验证,默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm 身份验证;
    6)Authenticator 会把相应的 token 传入 Realm,从 Realm 获取身份验证信息,如果没有返 回/抛出异常表示身份验证失败了。此处可以配置多个 Realm,将按照相应的顺序及策略进 行访问。

    Shiro身份认证分析到此为止,祝工作顺利,天天开心!

    相关文章

      网友评论

        本文标题:SHIRO源码解读——Authenticator身份认证

        本文链接:https://www.haomeiwen.com/subject/useurctx.html