Authenticator及AuthenticationStrategy

  • Authenticator的职责是验证用户,被subject的login()调用。

    public AuthenticationInfo authenticate(AuthenticationToken authenticationToken)
        throws AuthenticationException {
    
        //调用了Realm的getAuthenticationInfo()
    }
    
  • Authenticator:SecurityManager是其子接口,默认实现类为ModularRealmAuthenticator,其委托给多个Realm进行验证。
  • AuthenticatorStrategy:制定多个Realm的验证规则。默认提供的实现:
    • FirstSuccessfulStrategy:只要有一个Realm验证成功即可,值返回第一个验证成功的Realm的认证信息。
    • AtLeastSuccessfulStrategy:只要有一个Realm验证成功即可,返回所有验证成功的Realm的认证信息。
    • AllSuccessfulstrategy:默认使用;所有Realm都验证成功才行,返回所有认证信息。
  • 假设我们有三个Realm:
    • myRealm1: 用户名/密码为zhang/123时成功,且返回身份/凭据为zhang/123;
    • myRealm2: 用户名/密码为wang/123时成功,且返回身份/凭据为wang/123;
    • myRealm3: 用户名/密码为zhang/123时成功,且返回身份/凭据为zhang@163.com/123。
  • 现在测试一下AllSuccessfulStrategy验证策略
  • 首先是ini配置文件

    ;指定securityManager的authentication实现
    authenticator=org.apache.shiro.authc.pam.ModularRealmAuthenticator
    securityManager.authenticator=$authenticator
    ;指定securityManager.authenticator的authenticationStragegy
    allSuccessfulStrategy=org.apache.shiro.authc.pam.AllSuccessfulStrategy
    securityManager.authenticator.authenticationStrategy=$allSuccessfulStrategy
    
    myRealm1=com.haien.shiroHelloWorld.realm.MyRealm1
    myRealm2=com.haien.shiroHelloWorld.realm.MyRealm2
    myRealm3=com.haien.shiroHelloWorld.realm.MyRealm3
    securityManager.realms=$myRealm1,$myRealm3
    
  • 测试

    /**
     * @Author haien
     * @Description 测试AllSuccessfulStrategy成功
     * @Date 2019/2/16
     * @Param []
     * @return void
     **/
    @Test
    public void testAllSuccessfulStrategyWithSuccess(){
        //登录成功(因为ini配置文件中没有使用身份必须为wang的myRealm2)
        login("classpath:config/shiro-authenticator-all-success.ini"); //登录逻辑代码先通用化了
        Subject subject=SecurityUtils.getSubject();
    
        //得到一个身份集合,其包含了Realm验证成功的身份信息
        PrincipalCollection principalCollection=subject.getPrincipals();
        //包含了zhang和zhang@163.com的身份凭证
        Assert.assertEquals(2,principalCollection.asList().size());
    }
    
    /**
     * @Author haien
     * @Description 测试AllSuccessfulStrategy失败
     * @Date 2019/2/17
     * @Param []
     * @return void
     **/
    @Test(expected = UnknownAccountException.class) //括号里表明当方法抛出此异常时测试成功
    public void testAllSuccessfulAccountWithFail(){
        //但实际登录失败(配置文件改为使用myRealm1和myRealm2)
        login("classpath:config/shiro-authenticator-all-fail.ini");
        Subject subject=SecurityUtils.getSubject();
    }
    
  • 以上测试失败方法如果捕获异常的话可获得以下调用链

自定义AuthenticationStrategy

  • AuthenticationStrategy接口类

    //在所有Realm验证之前调用
    AuthenticationInfo beforeAllAttempts(Collection<? extends Realm> realms,
        AuthenticationToken token) throws AuthenticationException;
    
    //在每个Realm之前调用
    AuthenticationInfo beforeAttempt(Realm realm, AuthenticationToken token, 
        AuthenticationInfo aggregate) throws AuthenticationException;
    
    //在每个Realm之后调用
    AuthenticationInfo afterAttempt(Realm realm, AuthenticationToken token, 
        AuthenticationInfo singleRealmInfo, AuthenticationInfo aggregateInfo, Throwable t)
        throws AuthenticationException;
    
    //在所有Realm之后调用
    AuthenticationInfo afterAllAttempts(AuthenticationToken token, 
        AuthenticationInfo aggregate)throws AuthenticationException;
    
    //将所有验证成功的身份信息集合起来,便于最后一起返回;FirstSuccessfulStrategy类重写了该方法
    AuthenticationInfo merge(AuthenticationInfo info, AuthenticationInfo aggregate)
    
  • 自定义AuthenticationStrategy实现类一般继承AbstractAuthenticationStrategy即可。

  • AtLeastTwoAuthenticatorStrategy:至少两个通过才行,返回所有验证信息

    public class AtLeastTwoAuthenticatorStrategy extends AbstractAuthenticationStrategy {
        /**
         * @Author haien
         * @Description 在所有Realm验证之前调用
         * @Date 2019/2/17
         * @Param [realms, token]
         * @return org.apache.shiro.authc.AuthenticationInfo
         **/
        @Override
        public AuthenticationInfo beforeAllAttempts(Collection<? extends Realm> realms,
               AuthenticationToken token) throws AuthenticationException {
            //和父类方法一样,返回一个空的身份凭证
            return new SimpleAuthenticationInfo();
        }
    
        /**
         * @Author haien
         * @Description 在每个Realm验证之前调用
         * @Date 2019/2/17
         * @Param [realm, token, aggregate]
         * @return org.apache.shiro.authc.AuthenticationInfo
         **/
        @Override
        public AuthenticationInfo beforeAttempt(Realm realm, AuthenticationToken token,
               AuthenticationInfo aggregate) throws AuthenticationException {
            //和父类方法一样,返回之前合并的
            return aggregate;
        }
    
        @Override
        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{
                    //调用父类merge()将所有身份凭证集合起来,最后一起返回
                    info=merge(singleRealmInfo,aggregateInfo);
                }
            }
            return info;
        }
    
        /**
         * @Author haien
         * @Description 真正重写的方法:判断是否至少两个Realm通过
         * @Date 2019/2/17
         * @Param [token, aggregate]
         * @return org.apache.shiro.authc.AuthenticationInfo
         **/
        @Override
        public AuthenticationInfo afterAllAttempts(AuthenticationToken token,
               AuthenticationInfo aggregate) throws AuthenticationException {
    
            if(aggregate==null||CollectionUtils.isEmpty(aggregate.getPrincipals())
                    ||aggregate.getPrincipals().getRealmNames().size()<2){ //是否至少两个通过
                throw new AuthenticationException("Authentication token of type ["
                        +token.getClass()+"]"+"could not be authenticated by any 
                        configured realms.Please ensure that at least two realm 
                        can authenticate these tokens.");
            }
    
            //这一步即父类方法体
            return aggregate;
        }
    }
    
  • OnlyOneAuthenticatorStrategy:至少一个通过,重写的是afterAttempt()

    @Override
    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);
                if(info.getPrincipals().getRealmNames().size()>1){
                    System.out.println(info.getPrincipals().getRealmNames()); //打印集合
                    throw new AuthenticationException("Authentication token of type ["
                      +token.getClass()+"]"+"could not be authenticated by any configured " +
                            "realms. Please ensure that only one realm can authenticated " +
                            "these tokens");
                }
            }
        }
    
        return info;
    }
    
  • 代码实例:ideaProjects/shiroHelloWord/authenticationstrategy
  • 张开涛跟我学shiro第二章源码