乐趣区

关于java:手机短信登录邮箱登录QQ-登录都想要咋办

@[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);
    }
}

这个办法的逻辑很简略:

  1. 首先调用 assertRealmsConfigured 办法判断一下开发者有没有配置 Realm,要是没有配置就间接抛异样了。
  2. 判断开发者配置了几个 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 认证后果的聚合,具体逻辑如下:

  1. 如果以后 Realm 认证后果为 null,则把聚合后果赋值给 info 并返回。
  2. 如果以后 Realm 认证后果不为 null,并且聚合后果为 null,那么就把以后 Realm 的认证后果赋值给 info 并返回。
  3. 如果以后 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 的性能。

  1. 首先,零碎调用 beforeAllAttempts 办法会获取一个空的 SimpleAuthenticationInfo 对象作为聚合后果 aggregate。
  2. 接下来遍历所有的 Realm,在每个 Realm 调用之前先调用 beforeAttempt 办法,该办法只会一成不变的返回聚合后果 aggregate。
  3. 调用每个 Realm 的 getAuthenticationInfo 办法进行认证。
  4. 调用 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,而是:

  1. 如果聚合后果不为空,就间接返回聚合后果。
  2. 否则,如果以后认证后果不为空,就返回以后认证后果。
  3. 否则返回空。

能够看到,这里的 merge 其实就是筛选一个认证的 info 返回。如果后面有认证胜利的 Realm,前面 Realm 认证胜利后返回的 info 是不会被应用的。

2.4.6 小结

好啦,当初小伙伴们能够总结出 FirstSuccessfulStrategy 和 AtLeastOneSuccessfulStrategy 的区别了:

  1. AtLeastOneSuccessfulStrategy:当存在多个 Realm 的时候,即便曾经有一个 Realm 认证胜利了,前面的 Realm 也还是会去认证,并且如果前面的 Realm 也认证胜利了,那么会将多个 Realm 认证胜利的后果进行合并。
  2. 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 相干材料。

退出移动版