@[toc]
明天想和大家聊一聊 Shiro 中的多 Realm 认证策略问题~
在我的项目中,如果咱们想手机验证码登录、第三方 QQ 登录、邮箱登录等多种登录形式共存,那么就能够思考通过 Shiro 中的多 Realm 来实现,具体操作中,一个 Realm 刚好就对应一种登录形式。
多 Realm 登录的用法并不难,松哥之前也专门发过相干的文章和大家分享,传送门:
- 其实我不仅会 Spring Security,Shiro 也略懂一二!
明天我不想聊用法,次要是想和大家聊一聊这里相干的源码。因而本文须要大家有肯定的 Shiro 应用教训,若无,能够参考下面的链接恶补一下。
1. ModularRealmAuthenticator
1.1 Realm 去哪了?
咱们配置的 Realm,能够间接配置给 SecurityManager,也能够配置给 SecurityManager 中的 ModularRealmAuthenticator。
如果咱们是间接配置给 SecurityManager,那么在实现 Realm 的配置后,会主动调用 afterRealmsSet 办法,在该办法的中,会将咱们配置的所有 Realm 最终配置给 ModularRealmAuthenticator。
相干源码如下:
RealmSecurityManager#setRealm(RealmSecurityManager 是 DefaultWebSecurityManager 的父类)
public void setRealm(Realm realm) {if (realm == null) {throw new IllegalArgumentException("Realm argument cannot be null");
}
Collection<Realm> realms = new ArrayList<Realm>(1);
realms.add(realm);
setRealms(realms);
}
public void setRealms(Collection<Realm> realms) {if (realms == null) {throw new IllegalArgumentException("Realms collection argument cannot be null.");
}
if (realms.isEmpty()) {throw new IllegalArgumentException("Realms collection argument cannot be empty.");
}
this.realms = realms;
afterRealmsSet();}
能够看到,无论是设置单个 Realm 还是设置多个 Realm,最终都会调用到 afterRealmsSet 办法,该办法在 AuthorizingSecurityManager#afterRealmsSet 类中被重写,内容如下:
protected void afterRealmsSet() {super.afterRealmsSet();
if (this.authorizer instanceof ModularRealmAuthorizer) {((ModularRealmAuthorizer) this.authorizer).setRealms(getRealms());
}
}
能够看到,所有的 Realm 最终都被设置给 ModularRealmAuthenticator 了。
所以说,无论是单个 Realm 还是多个 Realm,最终都是由 ModularRealmAuthenticator 对立治理对立调用的。
1.2 ModularRealmAuthenticator 怎么玩
ModularRealmAuthenticator 中外围的办法就是 doAuthenticate,如下:
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);
}
}
这个办法的逻辑很简略:
- 首先调用 assertRealmsConfigured 办法判断一下开发者有没有配置 Realm,要是没有配置就间接抛异样了。
- 判断开发者配置了几个 Realm,要是配置了一个,就调用 doSingleRealmAuthentication 办法进行解决,要是配置了多个 Realm 则调用 doMultiRealmAuthentication 办法进行解决。
配置一个 Realm 的状况比较简单,不在本文的探讨范畴内,本文次要是想和大家探讨多个 Realm 的状况。
当存在多个 Realm 的时候,必然又会带来另一个问题: 认证策略 ,即怎么样就算认证胜利?一个 Realm 认证胜利就算胜利还是所有 Realm 认证胜利才算胜利?还是怎么样。
接下来咱们来具体聊一聊这个话题。
2. AuthenticationStrategy
先来整体上看下,负责认证策略的类是 AuthenticationStrategy,这是一个接口,有三个实现类:
单从字面上来看,三个实现类都好了解:
- AtLeastOneSuccessfulStrategy:至多有一个 Realm 认证胜利。
- AllSuccessfulStrategy:所有 Realm 都要认证胜利。
- FirstSuccessfulStrategy:这个从字面上了解不太精确, 它是只返回第一个认证胜利的用户数据 。
第二种其实很好了解,问题在于第 1 个和第 3 个,这两个独自了解也好了解,放在一起的话,那有人不禁要问,这俩有啥区别?
诚实说,在 1.3.2 之前的版本还真没啥大的区别,不过当初最新版本还是有些区别,且听松哥来剖析。
首先这里一共波及到四个办法:
- beforeAllAttempts:在所有 Realm 验证之前的做筹备。
- beforeAttempt:在单个 Realm 之前验证做筹备。
- afterAttempt:解决单个 Realm 验证之后的后续事宜。
- afterAllAttempts:解决所有 Realm 验证之后的后续事宜。
第一个和第四个办法在每次认证流程中只调用一次,而两头两个办法则在每个 Realm 调用前后都会被调用到,伪代码就相似上面这样:
下面这四个办法,在 AuthenticationStrategy 的四个实现类中有不同的实现,我整顿了上面一张表格,不便大家了解:
大家留神这里多了一个 merge 办法,这个办法是在 AbstractAuthenticationStrategy 类中定义的,当存在多个 Realm 时,合并多个 Realm 中的认证数据应用的。接下来咱们就依照这张表的程序,来挨个剖析这里的几个办法。
2.1 AbstractAuthenticationStrategy
2.1.1 beforeAllAttempts
间接来看代码吧:
public AuthenticationInfo beforeAllAttempts(Collection<? extends Realm> realms, AuthenticationToken token) throws AuthenticationException {return new SimpleAuthenticationInfo();
}
这里啥都没干,就创立了一个空的 SimpleAuthenticationInfo 对象。
2.1.2 beforeAttempt
public AuthenticationInfo beforeAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException {return aggregate;}
这个办法的逻辑也很简略,传入的 aggregate 参数是指多个 Realm 认证后聚合的后果,这里啥都没做,间接把后果一成不变返回。
2.1.3 afterAttempt
public AuthenticationInfo afterAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo singleRealmInfo, AuthenticationInfo aggregateInfo, Throwable t) throws AuthenticationException {
AuthenticationInfo info;
if (singleRealmInfo == null) {info = aggregateInfo;} else {if (aggregateInfo == null) {info = singleRealmInfo;} else {info = merge(singleRealmInfo, aggregateInfo);
}
}
return info;
}
这是每个 Realm 认证实现后要做的事件,参数 singleRealmInfo 示意单个 Realm 认证的后果,aggregateInfo 示意多个 Realm 认证后果的聚合,具体逻辑如下:
- 如果以后 Realm 认证后果为 null,则把聚合后果赋值给 info 并返回。
- 如果以后 Realm 认证后果不为 null,并且聚合后果为 null,那么就把以后 Realm 的认证后果赋值给 info 并返回。
- 如果以后 Realm 认证后果不为 null,并且聚合后果也不为 null,则将两者合并之后返回。
2.1.4 afterAllAttempts
public AuthenticationInfo afterAllAttempts(AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException {return aggregate;}
这里间接把聚合后果返回,没啥好说的。
2.1.5 merge
protected AuthenticationInfo merge(AuthenticationInfo info, AuthenticationInfo aggregate) {if( aggregate instanceof MergableAuthenticationInfo) {((MergableAuthenticationInfo)aggregate).merge(info);
return aggregate;
} else {
throw new IllegalArgumentException( "Attempt to merge authentication info from multiple realms, but aggregate" +
"AuthenticationInfo is not of type MergableAuthenticationInfo." );
}
}
merge 其实就是调用 aggregate 的 merge 办法进行合并,失常状况下咱们应用的 SimpleAuthenticationInfo 就是 MergableAuthenticationInfo 的子类,所以这里合并没问题。
2.2 AtLeastOneSuccessfulStrategy
2.2.1 beforeAllAttempts
同 2.1.1 大节。
2.2.2 beforeAttempt
同 2.1.2 大节。
2.2.3 afterAttempt
同 2.1.3 大节。
2.2.4 afterAllAttempts
public AuthenticationInfo afterAllAttempts(AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException {
//we know if one or more were able to successfully authenticate if the aggregated account object does not
//contain null or empty data:
if (aggregate == null || isEmpty(aggregate.getPrincipals())) {throw new AuthenticationException("Authentication token of type [" + token.getClass() + "]" +
"could not be authenticated by any configured realms. Please ensure that at least one realm can" +
"authenticate these tokens.");
}
return aggregate;
}
这里的逻辑很明确,就是当聚合后果为空就间接抛出异样。
2.2.5 merge
同 2.1.5 大节。
2.2.6 小结
联合 2.1 大节的内容,咱们来梳理一下 AtLeastOneSuccessfulStrategy 的性能。
- 首先,零碎调用 beforeAllAttempts 办法会获取一个空的 SimpleAuthenticationInfo 对象作为聚合后果 aggregate。
- 接下来遍历所有的 Realm,在每个 Realm 调用之前先调用 beforeAttempt 办法,该办法只会一成不变的返回聚合后果 aggregate。
- 调用每个 Realm 的 getAuthenticationInfo 办法进行认证。
- 调用 afterAttempt 办法对认证后果进行聚合解决。如果以后 Realm 认证返回 null,就把聚合后果返回;如果以后 Realm 认证不返回 null,就把 以后的 Realm 的认证后果和 aggregate 进行合并(aggregate 不会为 null,因为 beforeAllAttempts 办法中固定返回空对象)。
这就是 AtLeastOneSuccessfulStrategy 的认证策略。能够看到: 如果只有一个 Realm 认证胜利,那么失常返回,如果有多个 Realm 认证胜利,那么返回的用户信息中将蕴含多个认证用户信息。
能够通过如下形式获取返回的多个用户信息:
Subject subject = SecurityUtils.getSubject();
subject.login(token);
PrincipalCollection principals = subject.getPrincipals();
List list = principals.asList();
for (Object o : list) {System.out.println("o =" + o);
}
subject.getPrincipals() 办法能够获取多个认证胜利的凭证。
2.3 AllSuccessfulStrategy
2.3.1 beforeAllAttempts
同 2.1.1 大节。
2.3.2 beforeAttempt
public AuthenticationInfo beforeAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {if (!realm.supports(token)) {String msg = "Realm [" + realm + "] of type [" + realm.getClass().getName() + "] does not support" +
"the submitted AuthenticationToken [" + token + "]. The [" + getClass().getName() +
"] implementation requires all configured realm(s) to support and be able to process the submitted" +
"AuthenticationToken.";
throw new UnsupportedTokenException(msg);
}
return info;
}
能够看到,这里就是去查看了下 Realm 是否反对以后 token。
这块的代码我感觉略奇怪,为啥其余认证策略都不查看,只有这里查看?感觉像是一个 BUG。有懂行的小伙伴能够留言探讨下这个问题。
2.3.3 afterAttempt
public AuthenticationInfo afterAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo info, AuthenticationInfo aggregate, Throwable t)
throws AuthenticationException {if (t != null) {if (t instanceof AuthenticationException) {throw ((AuthenticationException) t);
} else {String msg = "Unable to acquire account data from realm [" + realm + "]. The [" +
getClass().getName() + "implementation requires all configured realm(s) to operate successfully" +
"for a successful authentication.";
throw new AuthenticationException(msg, t);
}
}
if (info == null) {String msg = "Realm [" + realm + "] could not find any associated account data for the submitted" +
"AuthenticationToken [" + token + "]. The [" + getClass().getName() + "] implementation requires" +
"all configured realm(s) to acquire valid account data for a submitted token during the" +
"log-in process.";
throw new UnknownAccountException(msg);
}
merge(info, aggregate);
return aggregate;
}
如果以后认证出错了,或者认证后果为 null,就间接抛出异样(因为这里要求每个 Realm 都认证胜利,凡是有一个认证失败了,前面的就没有必要认证了)。
如果所有都 OK,就会后果合并而后返回。
2.3.4 afterAllAttempts
同 2.1.4 大节。
2.3.5 merge
同 2.1.5 大节。
2.3.6 小结
这种策略比较简单,应该不必多做解释吧。如果有多个 Realm 认证胜利,这里也是会返回多个 Realm 的认证信息的,获取多个 Realm 的认证信息同上一大节。
2.4 FirstSuccessfulStrategy
2.4.1 beforeAllAttempts
public AuthenticationInfo beforeAllAttempts(Collection<? extends Realm> realms, AuthenticationToken token) throws AuthenticationException {return null;}
不同于后面,这里间接返回了 null。
2.4.2 beforeAttempt
public AuthenticationInfo beforeAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException {if (getStopAfterFirstSuccess() && aggregate != null && !isEmpty(aggregate.getPrincipals())) {throw new ShortCircuitIterationException();
}
return aggregate;
}
这里的逻辑是这样,如果 getStopAfterFirstSuccess() 办法返回 true,并且以后认证后果的聚合不为空,那么就间接抛出异样,一旦抛出异样,就会跳出以后循环,也就是不会调用以后 Realm 进行认证操作了。这个思路和 FirstSuccessfulStrategy 名字基本上是符合的。
不过这里有一个办法 getStopAfterFirstSuccess(),看名字就晓得是否在第一次胜利后进行认证,默认状况下,该变量为 false,即即便第一次认证胜利后,也还是会持续前面 Realm 的认证。
如果咱们心愿当第一次认证胜利后,前面的 Realm 就不认证了,那么记得配置该属性为 true。
2.4.3 afterAttempt
同 2.1.3 大节。
2.4.4 afterAllAttempts
同 2.1.4 大节。
2.4.5 merge
不晓得小伙伴们是否还记得 merge 办法是在哪里调用的,回顾 2.1.3 大节,如果以后 Realm 的认证和聚合后果都不为 null,就须要对后果进行合并,本来的合并是真正的去合并,这里重写了该办法,就没有去执行合并了:
protected AuthenticationInfo merge(AuthenticationInfo info, AuthenticationInfo aggregate) {if (aggregate != null && !isEmpty(aggregate.getPrincipals())) {return aggregate;}
return info != null ? info : aggregate;
}
这是三个策略中,惟一重写 merge 办法的。
这里的 merge 并没有真正的 merge,而是:
- 如果聚合后果不为空,就间接返回聚合后果。
- 否则,如果以后认证后果不为空,就返回以后认证后果。
- 否则返回空。
能够看到,这里的 merge 其实就是筛选一个认证的 info 返回。如果后面有认证胜利的 Realm,前面 Realm 认证胜利后返回的 info 是不会被应用的。
2.4.6 小结
好啦,当初小伙伴们能够总结出 FirstSuccessfulStrategy 和 AtLeastOneSuccessfulStrategy 的区别了:
- AtLeastOneSuccessfulStrategy:当存在多个 Realm 的时候,即便曾经有一个 Realm 认证胜利了,前面的 Realm 也还是会去认证,并且如果前面的 Realm 也认证胜利了,那么会将多个 Realm 认证胜利的后果进行合并。
- FirstSuccessfulStrategy:当存在多个 Realm 的时候,默认状况下,即便曾经有一个 Realm 认证胜利了,前面的 Realm 也还是会去认证,然而如果前面的 Realm 也认证胜利了,却并不会应用前面认证胜利的 Realm 返回的后果。如果咱们心愿当一个 Realm 认证胜利后,前面的 Realm 就不再认证了,那么能够配置 stopAfterFirstSuccess 属性的值,配置形式如下:
<bean class="org.apache.shiro.web.mgt.DefaultWebSecurityManager" id="securityManager">
<property name="authenticator">
<bean class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
<property name="authenticationStrategy">
<bean class="org.apache.shiro.authc.pam.FirstSuccessfulStrategy">
<property name="stopAfterFirstSuccess" value="true"/>
</bean>
</property>
<property name="realms">
<list>
<ref bean="myRealm01"/>
<ref bean="myRealm02"/>
</list>
</property>
</bean>
</property>
</bean>
3. 小结
好啦,这就是松哥和大家分享的 Shiro 多 Realm 状况,感兴趣的小伙伴能够去试试哦~
公众号后盾回复 shiro,获取 Shiro 相干材料。