Shiro源码分析----登录流程

在Shiro中,登录操作是由Subject的login()方法完成的,Subject是个接口,在Web环境中,实现类为WebDelegatingSubject,login方法从DeletatingSubject继承而来:

[java]  view plain  copy
  1. public void login(AuthenticationToken token) throws AuthenticationException {  
  2.     clearRunAsIdentitiesInternal();  
  3.     Subject subject = securityManager.login(this, token);  
  4.   
  5.     // 省略一些代码...  
  6. }  

由上可见,Subject.login()方法委托给了SecurityManager对象,在Web环境中,SecurityManager实现类为DefaultWebSecurityManager,其login方法从DefaultSecurityManager继承而来:

[java]  view plain  copy
  1. public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {  
  2.     AuthenticationInfo info;  
  3.     try {  
  4.         // 对提交的AuthenticationToken进行认证  
  5.         info = authenticate(token);  
  6.     } catch (AuthenticationException ae) {  
  7.         try {  
  8.             // 如果认证失败  
  9.             onFailedLogin(token, ae, subject);  
  10.         } catch (Exception e) {  
  11.             if (log.isInfoEnabled()) {  
  12.                 log.info("onFailedLogin method threw an " +  
  13.                         "exception.  Logging and propagating original AuthenticationException.", e);  
  14.             }  
  15.         }  
  16.         throw ae; //propagate,如果认证失败,使异常继续向上传播,从而返回至登录页面(见上篇)  
  17.     }  
  18.       
  19.     // 如果认证成功则重新创建Subject对象  
  20.     Subject loggedIn = createSubject(token, info, subject);  
  21.       
  22.     // 登录成功,主要处理RememberMe操作,即将登录信息存储在cookie中  
  23.     onSuccessfulLogin(token, info, loggedIn);  
  24.   
  25.     return loggedIn;  
  26. }  

最关键的authenticate方法:

[java]  view plain  copy
  1. public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {  
  2.     return this.authenticator.authenticate(token);  
  3. }  

SecurityManager把认证方法委托给认证器Authenticator的authenticate方法,Authenticator的实现类为:ModularRealmAuthenticator,其可以实现多认证信息源综合认证。ModularRealmAuthenticator实现使用了模版方法模式,随后执行doAuthenticate方法:

[java]  view plain  copy
  1. protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {  
  2.     assertRealmsConfigured();  
  3.     // Realm集合在为SecurityManager设置Realm时就会设置给Authenticator  
  4.     // 至于Realm代表什么,请参看:http://jinnianshilongnian.iteye.com/blog/2018936  
  5.     Collection<Realm> realms = getRealms();  
  6.     if (realms.size() == 1) {  
  7.         // 如果Realm只有一个  
  8.         return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);  
  9.     } else {  
  10.         // 如果Realm有多个  
  11.         return doMultiRealmAuthentication(realms, authenticationToken);  
  12.     }  
  13. }  

单一Realm认证:

[java]  view plain  copy
  1. protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {  
  2.     // 该Realm是否支持此种Token,因为并不是任何一种Realm与AuthenticationToken都是相互匹配的  
  3.     if (!realm.supports(token)) {  
  4.         String msg = "Realm [" + realm + "] does not support authentication token [" +  
  5.                 token + "].  Please ensure that the appropriate Realm implementation is " +  
  6.                 "configured correctly or that the realm accepts AuthenticationTokens of this type.";  
  7.         throw new UnsupportedTokenException(msg);  
  8.     }  
  9.     // 根据AuthenticationToken获取认证信息  
  10.     // Realm一般是由自己实现的,虽然说Shiro有一些自己的实现,但是在实际项目中,Shiro的实现直接就能使用的情况很少  
  11.     // 比较将认证信息(用户名密码等)存在数据库,则该getAuthenticationInfo方法就是根据Token中的信息去数据库中查找、  
  12.     // 匹配,如果匹配上了则返回相应认证后的认证信息  
  13.     AuthenticationInfo info = realm.getAuthenticationInfo(token);  
  14.     // 如果没有获取到则认证失败  
  15.     if (info == null) {  
  16.         String msg = "Realm [" + realm + "] was unable to find account data for the " +  
  17.                 "submitted AuthenticationToken [" + token + "].";  
  18.         throw new UnknownAccountException(msg);  
  19.     }  
  20.     return info;  
  21. }  

一般来说,自定义实现的Realm会继承自AuthenticatingRealm,所以会执行至:

[java]  view plain  copy
  1. public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {  
  2.       
  3.     // 先去缓存中查找,如果你使用了缓存,则不用每次都去文件或数据库中查找  
  4.     AuthenticationInfo info = getCachedAuthenticationInfo(token);  
  5.     if (info == null) {  
  6.         //otherwise not cached, perform the lookup:  
  7.         // 使用模版方法模式,进行直正的认证信息查找  
  8.         info = doGetAuthenticationInfo(token);  
  9.         log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);  
  10.         if (token != null && info != null) {  
  11.             cacheAuthenticationInfoIfPossible(token, info);  
  12.         }  
  13.     } else {  
  14.         log.debug("Using cached authentication info [{}] to perform credentials matching.", info);  
  15.     }  
  16.   
  17.     if (info != null) {  
  18.         // 断言AuthenticationToken与AuthenticationInfo是匹配的,简单点来说就是判断密码是否正确,不正确则抛异常  
  19.         // doGetAuthenticationInfo方法主要判断账户是否存在  
  20.         assertCredentialsMatch(token, info);  
  21.     } else {  
  22.         log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}].  Returning null.", token);  
  23.     }  
  24.   
  25.     return info;  
  26. }  

单一Realm认证时,只需要判断一个Realm认证是否成功即可,但是当存在多个Realm时情况就有点复杂了。因为有可能有些Realm认证成功了,有些Realm又认证失败了,这时到底算是认证成功还是失败呢?所以这时Shiro使用了策略模式,用具体的策略类来处理这个问题。多个Realm认证时的doMultiRealmAuthentication方法如下:

[java]  view plain  copy
  1. protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {  
  2.     // 首先就得获取认证策略,Shiro实现了三种:  
  3.     //1. AllSuccessfulStrategy: 必须所有Realm认证成功了才算是认证成功  
  4.     //2. AtLeastOneSuccessfulStrategy: 至少有一个Realm认证成功了就算是认证成功  
  5.     //3. FirstSuccessfulStrategy: 第一个Realm认证成功了就算是认证成功  
  6.     // 默认实现为AtLeastOneSuccessfulStrategy  
  7.     AuthenticationStrategy strategy = getAuthenticationStrategy();  
  8.       
  9.     // 假设我们现在使用的就是AtLeastOneSuccessfulStrategy  
  10.     // 返回SimpleAuthenticationInfo,这是一个空认证信息,并不含有principal与credentials  
  11.     AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);  
  12.   
  13.     if (log.isTraceEnabled()) {  
  14.         log.trace("Iterating through {} realms for PAM authentication", realms.size());  
  15.     }  
  16.   
  17.     for (Realm realm : realms) {  
  18.         // 直接返回aggregate  
  19.         aggregate = strategy.beforeAttempt(realm, token, aggregate);  
  20.   
  21.         if (realm.supports(token)) {  
  22.   
  23.             log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm);  
  24.   
  25.             AuthenticationInfo info = null;  
  26.             Throwable t = null;  
  27.             try {  
  28.                 info = realm.getAuthenticationInfo(token);  
  29.             } catch (Throwable throwable) {  
  30.                 t = throwable;  
  31.                 if (log.isDebugEnabled()) {  
  32.                     String msg = "Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:";  
  33.                     log.debug(msg, t);  
  34.                 }  
  35.             }  
  36.             //如果认证成功则info不为null,且包含有principal与credentials  
  37.             //afterAttempt方法会将info与aggregate合并,也就是将AuthenticationInfo的principal与credentials  
  38.             //分别用一集合存储  
  39.             aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);  
  40.   
  41.         } else {  
  42.             log.debug("Realm [{}] does not support token {}.  Skipping realm.", realm, token);  
  43.         }  
  44.     }  
  45.   
  46.     // 检测合并后的AuthenticationInfo中是否含用principal,如果有则返回aggregate  
  47.     // 没有则抛出异常认证失败,由此可见只要有一个Realm认证成功则算是认证成功  
  48.     aggregate = strategy.afterAllAttempts(token, aggregate);  
  49.   
  50.     return aggregate;  
  51. }  

上面只分析了AtLeastOneSuccessfulStrategy策略,其它两个请自行查看源码。


假设现在认证成功了,接下来执行DefaultSecurityManager.createSubject方法:

[java]  view plain  copy
  1. protected Subject createSubject(AuthenticationToken token, AuthenticationInfo info, Subject existing) {  
  2.     // 创建SubjectContext对象  
  3.     SubjectContext context = createSubjectContext();  
  4.     // 设置为已认证  
  5.     context.setAuthenticated(true);  
  6.     // 设置Token  
  7.     context.setAuthenticationToken(token);  
  8.     // 设置认证通过后的认证信息  
  9.     context.setAuthenticationInfo(info);  
  10.     if (existing != null) {  
  11.         // 设置先前存在的Subject  
  12.         context.setSubject(existing);  
  13.     }  
  14.     return createSubject(context);  
  15. }  
  16.   
  17. public Subject createSubject(SubjectContext subjectContext) {  
  18.     // 复制SubjectContext,原SubjectContext信息得以保留  
  19.     SubjectContext context = copy(subjectContext);  
  20.   
  21.     // 确保SubjectContext与SecurityManager关联  
  22.     context = ensureSecurityManager(context);  
  23.   
  24.     // 解析会话,有可能使用Servlet中的Session实现,也可能使用Shiro自己实现的Session  
  25.     context = resolveSession(context);  
  26.   
  27.     context = resolvePrincipals(context);  
  28.   
  29.     // 交由DefaultWebSubjectFactory.createSubject重新创建Subject  
  30.     Subject subject = doCreateSubject(context);  
  31.   
  32.     // 将Subject中的principal与credentials存储在Session中  
  33.     save(subject);  
  34.   
  35.     return subject;  
  36. }  

个人感觉createSubject重新创建是为了更新一些内部状态,至于为什么也没有去深入了解,望请高人指点一二。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值