最近几天在学习Apache Shiro......看了一些大神们的教程.....感觉收获不少.....但是毕竟教程也只是指引一下方向....即使是精品教程,仍然有很多东西都没有说明....所以自己也稍微研究了一下...记录了一下我的研究发现....教程点这里
这篇教程的最后提到了strategy.....然后给出了4个方法.....但是并没有怎么详细说明.....我想说说我的理解.....(我的理解可能会有很多错误)
我想先说说登陆验证的大致流程....大致......
Subject
从用户那里收集完用户名密码以后我们会调用subject.login(token)这个方法去登陆.....Subject是一个接口,没有定义login的具体实现.....Shiro里只有一个类实现了这个接口,是DelegatingSubject这个类.这个类里的方法login方法如下:
1 public void login(AuthenticationToken token) throws AuthenticationException { 2 clearRunAsIdentitiesInternal(); 3 Subject subject = securityManager.login(this, token); 4 5 PrincipalCollection principals; 6 7 String host = null; 8 9 if (subject instanceof DelegatingSubject) { 10 DelegatingSubject delegating = (DelegatingSubject) subject; 11 //we have to do this in case there are assumed identities - we don't want to lose the 'real' principals: 12 principals = delegating.principals; 13 host = delegating.host; 14 } else { 15 principals = subject.getPrincipals(); 16 } 17 18 if (principals == null || principals.isEmpty()) { 19 String msg = "Principals returned from securityManager.login( token ) returned a null or " + 20 "empty value. This value must be non null and populated with one or more elements."; 21 throw new IllegalStateException(msg); 22 } 23 this.principals = principals; 24 this.authenticated = true; 25 if (token instanceof HostAuthenticationToken) { 26 host = ((HostAuthenticationToken) token).getHost(); 27 } 28 if (host != null) { 29 this.host = host; 30 } 31 Session session = subject.getSession(false); 32 if (session != null) { 33 this.session = decorate(session); 34 } else { 35 this.session = null; 36 } 37 }
代码各种复杂=.= ..............我也看不懂....但是我看到第三行,明白了subject其实也是让securityManager来执行login操作的......那我们去看看securityManager吧......
SecurityManager
SecurityManager也是一个接口,各种继承,实现其他接口......还好实现类只有一个DefaultSecurityManager类...并且login方法直到这个类才被实现....
1 public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException { 2 AuthenticationInfo info; 3 try { 4 info = authenticate(token); 5 } catch (AuthenticationException ae) { 6 try { 7 onFailedLogin(token, ae, subject); 8 } catch (Exception e) { 9 if (log.isInfoEnabled()) { 10 log.info("onFailedLogin method threw an " + 11 "exception. Logging and propagating original AuthenticationException.", e); 12 } 13 } 14 throw ae; //propagate 15 } 16 17 Subject loggedIn = createSubject(token, info, subject); 18 19 onSuccessfulLogin(token, info, loggedIn); 20 21 return loggedIn; 22 }
我觉得这里最重要的就是第四行的authenticate方法.......
这个方法是在DefaultSecurityManager的N层父类AuthenticatingSecurityManager里实现的....
1 public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException { 2 return this.authenticator.authenticate(token); 3 }
这里又可以看出SecurityManager的login方法其实也是委托给认证器authenticator调用authenticate(token)方法来实现的.
public abstract class AuthenticatingSecurityManager extends RealmSecurityManager { /** * The internal <code>Authenticator</code> delegate instance that this SecurityManager instance will use * to perform all authentication operations. */ private Authenticator authenticator; /** * Default no-arg constructor that initializes its internal * <code>authenticator</code> instance to a * {@link org.apache.shiro.authc.pam.ModularRealmAuthenticator ModularRealmAuthenticator}. */ public AuthenticatingSecurityManager() { super(); this.authenticator = new ModularRealmAuthenticator(); } .................. }
认证器authenticator的默认实现是ModularRealmAuthenticator....this.authenticator = new ModularRealmAuthenticator();...所以我们再来看看ModularRealmAuthenticator..........
ModularRealmAuthenticator
ModularRealmAuthenticator的authenticate方法是继承自父类AbstractAuthenticator的..这个方法还是final的...
1 public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException { 2 3 if (token == null) { 4 throw new IllegalArgumentException("Method argumet (authentication token) cannot be null."); 5 } 6 7 log.trace("Authentication attempt received for token [{}]", token); 8 9 AuthenticationInfo info; 10 try { 11 info = doAuthenticate(token); 12 if (info == null) { 13 String msg = "No account information found for authentication token [" + token + "] by this " + 14 "Authenticator instance. Please check that it is configured correctly."; 15 throw new AuthenticationException(msg); 16 } 17 } catch (Throwable t) { 18 AuthenticationException ae = null; 19 if (t instanceof AuthenticationException) { 20 ae = (AuthenticationException) t; 21 } 22 if (ae == null) { 23 //Exception thrown was not an expected AuthenticationException. Therefore it is probably a little more 24 //severe or unexpected. So, wrap in an AuthenticationException, log to warn, and propagate: 25 String msg = "Authentication failed for token submission [" + token + "]. Possible unexpected " + 26 "error? (Typical or expected login exceptions should extend from AuthenticationException)."; 27 ae = new AuthenticationException(msg, t); 28 } 29 try { 30 notifyFailure(token, ae); 31 } catch (Throwable t2) { 32 if (log.isWarnEnabled()) { 33 String msg = "Unable to send notification for failed authentication attempt - listener error?. " + 34 "Please check your AuthenticationListener implementation(s). Logging sending exception " + 35 "and propagating original AuthenticationException instead..."; 36 log.warn(msg, t2); 37 } 38 } 39 40 41 throw ae; 42 } 43 44 log.debug("Authentication successful for token [{}]. Returned account [{}]", token, info); 45 46 notifySuccess(token, info); 47 48 return info; 49 }
从中我们可以看出比较重要的是第11行 info = doAuthenticate(token);
doAuthenticate(token)方法在ModularRealmAuthenticator之中被override过...
1 protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { 2 assertRealmsConfigured(); 3 Collection<Realm> realms = getRealms(); 4 if (realms.size() == 1) { 5 return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken); 6 } else { 7 return doMultiRealmAuthentication(realms, authenticationToken); 8 } 9 }
这里还是比较明显的,根据定义的Realm数量来决定是调用doSingleRealmAuthentication方法还是调用doMultiRealmAuthentication方法...
我们来看看doMultiRealmAuthentication方法里到底做了些什么呢..这个方法是定义在ModularRealmAuthenticator里的...
1 protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) { 2 3 AuthenticationStrategy strategy = getAuthenticationStrategy(); 4 5 AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token); 6 7 if (log.isTraceEnabled()) { 8 log.trace("Iterating through {} realms for PAM authentication", realms.size()); 9 } 10 11 for (Realm realm : realms) { 12 13 aggregate = strategy.beforeAttempt(realm, token, aggregate); 14 15 if (realm.supports(token)) { 16 17 log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm); 18 19 AuthenticationInfo info = null; 20 Throwable t = null; 21 try { 22 info = realm.getAuthenticationInfo(token); 23 } catch (Throwable throwable) { 24 t = throwable; 25 if (log.isDebugEnabled()) { 26 String msg = "Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:"; 27 log.debug(msg, t); 28 } 29 } 30 31 aggregate = strategy.afterAttempt(realm, token, info, aggregate, t); 32 33 } else { 34 log.debug("Realm [{}] does not support token {}. Skipping realm.", realm, token); 35 } 36 } 37 38 aggregate = strategy.afterAllAttempts(token, aggregate); 39 40 return aggregate; 41 }
虽然很多看不明白....但是我大致流程能看懂:
在调用每个Realm之前,先执行strategy.beforeAllAttempts(realms, token);
然后再进入 for (Realm realm : realms),就是准备去调用每个realm来认证..不过每个Realm认证之前还要调用aggregate = strategy.beforeAttempt(realm, token, aggregate);
然后得到单个Realm的认证结果 info = realm.getAuthenticationInfo(token);
再调用aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);这里可能会把info的认证信息和aggregate合并,就是把principle合并....不过还是要看具体的strategy的.
最后再调用 aggregate = strategy.afterAllAttempts(token, aggregate);
也就是说strategy.beforeAllAttempts(realms, token);调用1次,然后根据realm的数量调用N次strategy.beforeAttempt(realm, token, aggregate);N次realm.getAuthenticationInfo(token);N次 strategy.afterAttempt(realm, token, info, aggregate, t);再调用1次strategy.afterAllAttempts(token, aggregate);
另外,不是说你单个Realm里认证失败,比如抛出了UnknownAccountException..所有认证就终止了....仔细看第23,24行会发现Realm里抛出的异常是会被拦截下来的...然后传给了 strategy.afterAttempt(realm, token, info, aggregate, t);....发现了这个异常以后到底是算认证失败了还是继续看其他的Realm是根据strategy来的...比如AllSuccessfulStrategy的afterAttempt方法...发现了任意1个realm抛出的异常,就直接抛出异常,终止认证....但是AtLeastOneSuccessfulStrategy就不一样...它就会无视这个异常...继续下面realm来认证...然后根据afterAllAttempts里的AuthenticationInfo的汇总信息来判断到底是认证失败了要抛出异常还是认证是成功的....
Shiro默认的策略是AtLeastOneSuccessfulStrategy
各种Strategy
我觉得看到这里我们可以再来看看Shiro已经定义的三种Strategy了...教程里提到过:
FirstSuccessfulStrategy:只要有一个Realm验证成功即可,只返回第一个Realm身份验证成功的认证信息,其他的忽略;
AtLeastOneSuccessfulStrategy:只要有一个Realm验证成功即可,和FirstSuccessfulStrategy不同,返回所有Realm身份验证成功的认证信息;
AllSuccessfulStrategy:所有Realm验证成功才算成功,且返回所有Realm身份验证成功的认证信息,如果有一个失败就失败了。
这3种策略都继承自AbstractAuthenticationStrategy类..这个抽象类里定义了前面提到的2个after方法和2个before方法还有1个merge方法...
1 protected AuthenticationInfo merge(AuthenticationInfo info, AuthenticationInfo aggregate) { 2 if( aggregate instanceof MergableAuthenticationInfo ) { 3 ((MergableAuthenticationInfo)aggregate).merge(info); 4 return aggregate; 5 } else { 6 throw new IllegalArgumentException( "Attempt to merge authentication info from multiple realms, but aggregate " + 7 "AuthenticationInfo is not of type MergableAuthenticationInfo." ); 8 } 9 }
merge方法会调用AuthenticationInfo的merge方法....大致过程是:
public void merge(AuthenticationInfo info) { if (info == null || info.getPrincipals() == null || info.getPrincipals().isEmpty()) { return; } if (this.principals == null) { this.principals = info.getPrincipals(); } else { if (!(this.principals instanceof MutablePrincipalCollection)) { this.principals = new SimplePrincipalCollection(this.principals); } ((MutablePrincipalCollection) this.principals).addAll(info.getPrincipals()); } ...................... }
大致意思就是把info的principals合并到aggregate的principals上去....
AbstractAuthenticationStrategy的merge方法主要是在afterAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo singleRealmInfo, AuthenticationInfo aggregateInfo, Throwable t)里被调用,因为经过一个Realm的认证以后会得到info对象...这个时候不同的认证策略就需要考虑是不是要把当前得到的info合并到已经有的前面几个Realm积累下来的aggregateInfo里去了...
举例:
AllSuccessfulStrategy类
1 public AuthenticationInfo beforeAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException { 2 if (!realm.supports(token)) { 3 String msg = "Realm [" + realm + "] of type [" + realm.getClass().getName() + "] does not support " + 4 " the submitted AuthenticationToken [" + token + "]. The [" + getClass().getName() + 5 "] implementation requires all configured realm(s) to support and be able to process the submitted " + 6 "AuthenticationToken."; 7 throw new UnsupportedTokenException(msg); 8 } 9 10 return info; 11 }
第2行,如果realm不支持token,显然这个realm是认证失败的...而AllSuccessfulStrategy策略需要所有Realm都认证成功才行...果断抛出异常...
1 public AuthenticationInfo afterAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo info, AuthenticationInfo aggregate, Throwable t) 2 throws AuthenticationException { 3 if (t != null) { 4 if (t instanceof AuthenticationException) { 5 //propagate: 6 throw ((AuthenticationException) t); 7 } else { 8 String msg = "Unable to acquire account data from realm [" + realm + "]. The [" + 9 getClass().getName() + " implementation requires all configured realm(s) to operate successfully " + 10 "for a successful authentication."; 11 throw new AuthenticationException(msg, t); 12 } 13 } 14 if (info == null) { 15 String msg = "Realm [" + realm + "] could not find any associated account data for the submitted " + 16 "AuthenticationToken [" + token + "]. The [" + getClass().getName() + "] implementation requires " + 17 "all configured realm(s) to acquire valid account data for a submitted token during the " + 18 "log-in process."; 19 throw new UnknownAccountException(msg); 20 } 21 22 log.debug("Account successfully authenticated using realm [{}]", realm); 23 24 // If non-null account is returned, then the realm was able to authenticate the 25 // user - so merge the account with any accumulated before: 26 merge(info, aggregate); 27 28 return aggregate; 29 }
Throwable t, t是从Realm里抛出来的,如果Realm抛出了异常,不管什么原因,认证肯定是失败了...所以Strategy也要抛出异常,即认证失败了......info = null的道理也是一样的....
AtLeastOneSuccessfulStrategy类:
1 public AuthenticationInfo afterAllAttempts(AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException { 2 //we know if one or more were able to succesfully authenticate if the aggregated account object does not 3 //contain null or empty data: 4 if (aggregate == null || CollectionUtils.isEmpty(aggregate.getPrincipals())) { 5 throw new AuthenticationException("Authentication token of type [" + token.getClass() + "] " + 6 "could not be authenticated by any configured realms. Please ensure that at least one realm can " + 7 "authenticate these tokens."); 8 } 9 10 return aggregate; 11 }
所有Realm的认证都执行完毕之后,如果AuthenticationInfo 的合并结果aggregate还是null或者空的话那么说明所有Realm的认证都失败的...那么就应该抛出异常,说明认证失败了....
FirstSuccessfulStrategy类:
1 public class FirstSuccessfulStrategy extends AbstractAuthenticationStrategy { 2 3 /** 4 * Returns {@code null} immediately, relying on this class's {@link #merge merge} implementation to return 5 * only the first {@code info} object it encounters, ignoring all subsequent ones. 6 */ 7 public AuthenticationInfo beforeAllAttempts(Collection<? extends Realm> realms, AuthenticationToken token) throws AuthenticationException { 8 return null; 9 } 10 11 /** 12 * Returns the specified {@code aggregate} instance if is non null and valid (that is, has principals and they are 13 * not empty) immediately, or, if it is null or not valid, the {@code info} argument is returned instead. 14 * <p/> 15 * This logic ensures that the first valid info encountered is the one retained and all subsequent ones are ignored, 16 * since this strategy mandates that only the info from the first successfully authenticated realm be used. 17 */ 18 protected AuthenticationInfo merge(AuthenticationInfo info, AuthenticationInfo aggregate) { 19 if (aggregate != null && !CollectionUtils.isEmpty(aggregate.getPrincipals())) { 20 return aggregate; 21 } 22 return info != null ? info : aggregate; 23 } 24 }
为什么要override beforeAllAttempts方法返回null我也不懂....父类是返回new SimpleAuthenticationInfo();.....
override merge方法是因为FirstSuccessfulStrategy策略只返回第一个验证成功的Realm的AuthenticationInfo ...
如果aggregate不是空的或者null,说明前面的Realm有成功过,那么根据策略,这个时候不应该把本次realm得到的info合并进去.....而是直接返回前面验证成功的AuthenticationInfo ......