在Shiro中,登陆操做是由Subject的login()
方法完成的,Subject
是个接口,在Web环境中,实现类为WebDelegatingSubject
,login
方法从DeletatingSubject
继承而来:java
public void login(AuthenticationToken token) throws AuthenticationException { clearRunAsIdentitiesInternal(); Subject subject = securityManager.login(this, token); // 省略一些代码... }
由上可见,Subject.login()
方法委托给了SecurityManager
对象,在Web环境中,SecurityManager
实现类为DefaultWebSecurityManager
,其login
方法从DefaultSecurityManager
继承而来:数据库
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException { AuthenticationInfo info; try { // 对提交的AuthenticationToken进行认证 info = authenticate(token); } catch (AuthenticationException ae) { try { // 若是认证失败 onFailedLogin(token, ae, subject); } catch (Exception e) { if (log.isInfoEnabled()) { log.info("onFailedLogin method threw an " + "exception. Logging and propagating original AuthenticationException.", e); } } throw ae; //propagate,若是认证失败,使异常继续向上传播,从而返回至登陆页面(见上篇) } // 若是认证成功则从新建立Subject对象 Subject loggedIn = createSubject(token, info, subject); // 登陆成功,主要处理RememberMe操做,即将登陆信息存储在cookie中 onSuccessfulLogin(token, info, loggedIn); return loggedIn; }
最关键的authenticate
方法:设计模式
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException { return this.authenticator.authenticate(token); }
SecurityManager
把认证方法委托给认证器Authenticator
的authenticate
方法,Authenticator
的实现类为:ModularRealmAuthenticator
,其能够实现多认证信息源综合认证。ModularRealmAuthenticator
实现使用了模版方法设计模式,随后执行doAuthenticate
方法:缓存
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { assertRealmsConfigured(); // Realm集合在为SecurityManager设置Realm时就会设置给Authenticator // 至于Realm表明什么,请参看:http://jinnianshilongnian.iteye.com/blog/2018936 Collection<Realm> realms = getRealms(); if (realms.size() == 1) { // 若是Realm只有一个 return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken); } else { // 若是Realm有多个 return doMultiRealmAuthentication(realms, authenticationToken); } }
单一Realm认证:cookie
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) { // 该Realm是否支持此种Token,由于并非任何一种Realm与AuthenticationToken都是相互匹配的 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); } // 根据AuthenticationToken获取认证信息 // Realm通常是由本身实现的,虽说Shiro有一些本身的实现,可是在实际项目中,Shiro的实现直接就能使用的状况不多 // 比较将认证信息(用户名密码等)存在数据库,则该getAuthenticationInfo方法就是根据Token中的信息去数据库中查找、 // 匹配,若是匹配上了则返回相应认证后的认证信息 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); } return info; }
通常来讲,自定义实现的Realm会继承自AuthenticatingRealm
,因此会执行至:app
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { // 先去缓存中查找,若是你使用了缓存,则不用每次都去文件或数据库中查找 AuthenticationInfo info = getCachedAuthenticationInfo(token); if (info == null) { //otherwise not cached, perform the lookup: // 使用模版方法模式,进行直正的认证信息查找 info = doGetAuthenticationInfo(token); log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info); if (token != null && info != null) { cacheAuthenticationInfoIfPossible(token, info); } } else { log.debug("Using cached authentication info [{}] to perform credentials matching.", info); } if (info != null) { // 断言AuthenticationToken与AuthenticationInfo是匹配的,简单点来讲就是判断密码是否正确,不正确则抛异常 // doGetAuthenticationInfo方法主要判断帐户是否存在 assertCredentialsMatch(token, info); } else { log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token); } return info; }
单一Realm认证时,只须要判断一个Realm认证是否成功便可,可是当存在多个Realm时状况就有点复杂了。由于有可能有些Realm认证成功了,有些Realm又认证失败了,这时到底算是认证成功仍是失败呢?因此这时Shiro使用了策略模式,用具体的策略类来处理这个问题。多个Realm认证时的doMultiRealmAuthentication
方法以下:ide
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) { // 首先就得获取认证策略,Shiro实现了三种: //1. AllSuccessfulStrategy: 必须全部Realm认证成功了才算是认证成功 //2. AtLeastOneSuccessfulStrategy: 至少有一个Realm认证成功了就算是认证成功 //3. FirstSuccessfulStrategy: 第一个Realm认证成功了就算是认证成功 // 默认实现为AtLeastOneSuccessfulStrategy AuthenticationStrategy strategy = getAuthenticationStrategy(); // 假设咱们如今使用的就是AtLeastOneSuccessfulStrategy // 返回SimpleAuthenticationInfo,这是一个空认证信息,并不含有principal与credentials AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token); if (log.isTraceEnabled()) { log.trace("Iterating through {} realms for PAM authentication", realms.size()); } for (Realm realm : realms) { // 直接返回aggregate aggregate = strategy.beforeAttempt(realm, token, aggregate); if (realm.supports(token)) { log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm); AuthenticationInfo info = null; Throwable t = null; try { info = realm.getAuthenticationInfo(token); } catch (Throwable throwable) { t = throwable; if (log.isDebugEnabled()) { String msg = "Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:"; log.debug(msg, t); } } //若是认证成功则info不为null,且包含有principal与credentials //afterAttempt方法会将info与aggregate合并,也就是将AuthenticationInfo的principal与credentials //分别用一集合存储 aggregate = strategy.afterAttempt(realm, token, info, aggregate, t); } else { log.debug("Realm [{}] does not support token {}. Skipping realm.", realm, token); } } // 检测合并后的AuthenticationInfo中是否含用principal,若是有则返回aggregate // 没有则抛出异常认证失败,因而可知只要有一个Realm认证成功则算是认证成功 aggregate = strategy.afterAllAttempts(token, aggregate); return aggregate; }
上面只分析了AtLeastOneSuccessfulStrategy
策略,其它两个请自行查看源码。this
假设如今认证成功了,接下来执行DefaultSecurityManager.createSubject
方法:debug
protected Subject createSubject(AuthenticationToken token, AuthenticationInfo info, Subject existing) { // 建立SubjectContext对象 SubjectContext context = createSubjectContext(); // 设置为已认证 context.setAuthenticated(true); // 设置Token context.setAuthenticationToken(token); // 设置认证经过后的认证信息 context.setAuthenticationInfo(info); if (existing != null) { // 设置先前存在的Subject context.setSubject(existing); } return createSubject(context); } public Subject createSubject(SubjectContext subjectContext) { // 复制SubjectContext,原SubjectContext信息得以保留 SubjectContext context = copy(subjectContext); // 确保SubjectContext与SecurityManager关联 context = ensureSecurityManager(context); // 解析会话,有可能使用Servlet中的Session实现,也可能使用Shiro本身实现的Session context = resolveSession(context); context = resolvePrincipals(context); // 交由DefaultWebSubjectFactory.createSubject从新建立Subject Subject subject = doCreateSubject(context); // 将Subject中的principal与credentials存储在Session中 save(subject); return subject; }
-------------------------------- END -------------------------------设计
及时获取更多精彩文章,请关注公众号《Java精讲》。