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

身份认证,即在应用中证明他就是他本人。一般提供如他们的身份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身份认证分析到此为止,祝工作顺利,天天开心!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,539评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,594评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,871评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,963评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,984评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,763评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,468评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,850评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,002评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,144评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,823评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,483评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,026评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,150评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,415评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,092评论 2 355

推荐阅读更多精彩内容