2021年你还不会Shiro?----3.分析身份认证源码实现自定义Realm

前言

我们已经知道无论我们是认证还是授权,数据的获取都是来源于Realm,Realm就相当于我们的datasource,在上一篇中我们使用的是用IniRealm来加载我们的配置文件shiro.ini,同时我们也说了ini只是临时解决方案,在实际的开发中是不可能把用户信息和权限信息放在ini文件中的,都是来源于数据库,那么系统提供的IniRealm就不能满足我们的需要了,我们就需要自定义Realm来实现真正的场景,事实上ini文件也只是apache为我们提供学习使用的策略,下面我们就来看下怎么自己定义一个Realm。

一.用户名与密码的默认获取

认证的实现在代码中就一行,如下:

subject.login(authenticationToken);

之前的代码都是为了这一行代码做准备,之前不是说Shiro的身份认证是通过Authenticator认证器来实现的吗,这里只是subject调用了登录方法就可以了?这和前面所说的Shiro的架构也不相符啊?下面我们一起来看一看。

1.用户名的验证

我们debug运行以下代码:

public class TestAuthenticator {
    
    
    public static void main(String[] args) {
    
    
        //第一步,获取Security Manager
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        //第二步,使用Security Manager加载配置文件,注意前面已经说过Realm是身份认证与权限的数据获取的地方,所以调用setRealm
        defaultSecurityManager.setRealm(new IniRealm("classpath:shiro.ini"));
        //第三步,使用SecurityUtils获取Subject
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        Subject subject = SecurityUtils.getSubject();
        //第四步,获取用户登录Token
        UsernamePasswordToken authenticationToken = new UsernamePasswordToken("zhaoyun","daye");
        try {
    
    
            System.out.println(subject.isAuthenticated());
            //第五步,登录
            subject.login(authenticationToken);
            System.out.println(subject.isAuthenticated());
        } catch (UnknownAccountException e) {
    
    
            e.printStackTrace();
        }catch(IncorrectCredentialsException e){
    
    
            e.printStackTrace();
        }catch(Exception e){
    
    
            e.printStackTrace();
        }

    }
}

我们知道subject.login()才是核心的内容,我们dubug运行进入,看一看该方法的底层到底是怎么实现的身份认证的。
点击进去后,我们进入到了DelegatingSubject中的login方法,如下图:
在这里插入图片描述
进入的不是subject的login方法,这里DelegatingSubject其实是Subject的实现类,但是在这个方法中我们发现也并没有对用户信息进行判断,而是又是拿着token继续调用了,那么我们继续点进去看一看,如下图,我们进入到了DefaultSecuityManager这个安全管理器中的login方法了,说明Subject底层调用的仍是安全管理器中的认证方法。
在这里插入图片描述
但是很显然,这里并不是终点,这里继续将Token传递了下去,我们还得继续跟进,然后我们发现进入到了AuthentticatingSecurityManager这个安全管理器如下图:
在这里插入图片描述
不过即使已经封装了这么多层,我们发现到了这一步依然没有对token进行处理,还是在继续调用,我们还需要继续跟踪调用,然后进入了AbstractAuthenticator这个认证器中如下图:
在这里插入图片描述

但是这个认证器还不是终点,继续跟进,进入了ModularRealmAuthenticator这个类中,如下图:
在这里插入图片描述
然后我们可以发现在该方法的最后一行代码中,继续是在调用,仍然没有处理,继续跟进后发现进入到了该类中的另一个方法,该方法里作了是否支持token等的校验,开始校验token了,说明我们离目的地不远了,继续跟进代码,发现进入到了AutenticatingRealm这个类中,如下图:
在这里插入图片描述
这里先去缓存中获取是否认证了该Token代表的用户,明显我们缓存中是没有数据的,所以进入到了第三行,我们还需要继续跟进,点进去后进入到了SimpleAccountRealm类的doGetAuthenticationInfo中,如下图:
在这里插入图片描述
从代码中我们可以清晰的看到,Token并没有继续往下继续传递了,很明显这里就是我们要找的代码调用的终点站,SimpleAccountRealm的doGetAuthenticationInfo方法这里就是真正实现了认证的地方,我们可以发现Shiro对认证的封装很深,找起来真的有些费劲,下面我们一起看下该方法的内容,如下:

   protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    
    
       UsernamePasswordToken upToken = (UsernamePasswordToken)token;
       SimpleAccount account = this.getUser(upToken.getUsername());
       if (account != null) {
    
    
           if (account.isLocked()) {
    
    
               throw new LockedAccountException("Account [" + account + "] is locked.");
           }

           if (account.isCredentialsExpired()) {
    
    
               String msg = "The credentials for account [" + account + "] are expired";
               throw new ExpiredCredentialsException(msg);
           }
       }

       return account;
   }

第一行代码将token强转成usernamePasswordToken类型,事实上我们使用的就是该类型。
第二行代码获取token中的用户名然后传递给该类的getUser对象然后返回一个SimpleAccount对象,我们已经知道token.getUserName肯定就是我们传入的用户名了,拿着这个用户名调用getUser又是干什么用的呢?我们进入该方法看下:
在这里插入图片描述
进入该方法后我们发现这个方法拿到的就是我们配置的ini文件的信息,那么就知道了这个方法就是校验我们用户名是否存在的方法。我们返回到上一层的代码中,这里接收的是一个SimpleAccount对象,很显然只要接收的对象不是null就代表用户名验证成功了,下面的方法是对用户是否被锁住以及用户凭证是否过期的验证,到这里我们发现用户名是验证完了用户名也正确,但是怎么没有验证密码呢?

2.密码的验证

在上面我们已经找到了用户名是如何验证的,密码却没有看到,现在我们在继续刚刚的代码继续往下走,寻找密码的验证地方,F8执行本方法回到发现该方法最终返回了一个SimpleAccount对象,既然用户名已经验证通过了还返回SimpleAccount肯定就是为了密码验证呗,跟着代码返现这个SimpleAccount对象返回到了这里,如下图:
在这里插入图片描述
调用返回到了这里之后呢,我们可以发现先是将用户名放进了缓存,然后急着会进入到图中标红的部分,根据名称我们可以看出这部分是断言凭证信息是否匹配的传入的是用户的token与ini文件中查到的用户,那么这里肯定就是实现密码校验的地方了,我们点击进去看一下这部分的实现,如下图:
在这里插入图片描述
很明显标注的这一行就是判断密码是否匹配的了,下面在不匹配的场景也抛出了IncorrectCredentialsException该异常,也是我们说的密码不匹配异常,再从图中标注的位置进入下一层调用,如下图:
在这里插入图片描述
我们可以发现,这里是根据token和info中的密码生成了两个Object对象,这两个Object对象其实就是密码,然后使用equals对这两个对象进行比对,匹配成功自然就是验证通过了,否则验证失败返回false回到上一层,就会抛出无效的身份凭证异常,到这里我们已经用户名和密码都验证完了。

3.总结用户名和密码验证

用户名与密码的验证过程上面已经说了一遍,这里就不在过多赘述了,通过上面的过程我们可以明了用户名是通过SimpleAccountRealm中的doGetAuthenticationInfo方法来实现验证的而密码的验证则是在AuthenticatingRealm中的assertCredentialsMatch实现的,观察密码的验证我们可以发现,这里好像并不需要我们作什么,我们只需要在用户验证时返回用户信息即可(包含密码信息)到了这里程序就会拿着用户登录信息,与配置文件中的信息进行校验,匹配则是验证通过了,即使我们使用数据库获取用户信息,我们还是一样返回一个SimpleAccout信息即可,到这里还是自动判断的,所以我们可以得出,密码的验证时不需要我们人为干预的,Shiro会自动帮我们实现密码的认证,如果对上方的调用比较熟悉的话我们还会发现用户认证的真正实现类SimpleAccountRealm,正是密码认证的真正实现类AuthenticatingRealm的子类。

二.自定义Realm

根据上方的验证过程我们可以发现,我们如果想要实现自己的Realm那么久需要继承AuthenticatingRealm,像SimpleAccountRealm那样去实现doGetAuthenticationInfo方法就可以了。

1.自定义Realm----FirstRealm

下面我们去自顶一个自己的Realm叫FirstRealm。

public class FirstRealm extends AuthenticatingRealm {
    
    
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    
    
        
        return null;
    }
}

我们已经定义好了自己的Realm,但是现在方法中什么都没有,返回的信息就是空,那么在进行比对时就会报未知的用户名也就是我们说的UnknownAccountException这个异常,这里我们先不从数据库获取密码,依然采用写死的方式进行比对,从数据库获取数据相信有基础的同学都是会的,这里就不写了,我们参考SimpleAccountRealm的实现类完成该方法,如下:

public class FirstRealm extends AuthenticatingRealm {
    
    
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    
    
        UsernamePasswordToken upToken = (UsernamePasswordToken)authenticationToken;
        String userName = upToken.getUsername();
        //假设这就是从数据库获取的用户信息
        SimpleAccount simpleAccount = getAccountInfo();
        if(simpleAccount.getPrincipals().asList().contains(userName)) {
    
    
            System.out.println("用户名验证通过");
            return simpleAccount;
        }else{
    
    
            return null;
//            throw new UnknownAccountException();
        }
    }

    private SimpleAccount getAccountInfo(){
    
    
        return new SimpleAccount("zhaoyun","1111",this.getName());
//        return  new SimpleAuthenticationInfo("zhaoyun","1111",this.getName());
    }
}

从上方的代码中我们可以看到有两处代码被我注释掉了,注意第一处是在retun null的地方,这里我们可以选择手动抛出一个未知用户的异常,也可以直接抛出一个null,上层调用会帮我们抛出未知用户异常,所以这里不抛异常没有关系,第二处代码是在假装获取数据库数据的地方,这里之缩写写了两个两种不同的返回,是因为我们用这两种方式是都可以的,有兴趣的可以换成另一种试下,当然顺带的返回值也要改变。

2.使用自定义Realm

我们在写身份认证的代码时,当时注入的是IniRealm,那么我们换成自己实现的Realm就可以了,如下:

public class TestAuthenticator {
    
    
    public static void main(String[] args) {
    
    
        //第一步,获取Security Manager
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        //第二步,使用Security Manager加载配置文件,注意前面已经说过Realm是身份认证与权限的数据获取的地方,所以调用setRealm
        defaultSecurityManager.setRealm(new FirstRealm());
        //第三步,使用SecurityUtils获取Subject
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        Subject subject = SecurityUtils.getSubject();
        //第四步,获取用户登录Token
        UsernamePasswordToken authenticationToken = new UsernamePasswordToken("zhaoyun","daye");
        try {
    
    
            System.out.println(subject.isAuthenticated());
            //第五步,登录
            subject.login(authenticationToken);
            System.out.println(subject.isAuthenticated());
        } catch (UnknownAccountException e) {
    
    
            e.printStackTrace();
        }catch(IncorrectCredentialsException e){
    
    
            e.printStackTrace();
        }catch(Exception e){
    
    
            e.printStackTrace();
        }

    }
}

然后我们运行下上访的代码,感受下通过自己定义的Realm是否好使,运行结果如下:

false
用户名验证通过
org.apache.shiro.authc.IncorrectCredentialsException: Submitted credentials for token [org.apache.shiro.authc.UsernamePasswordToken - zhaoyun, rememberMe=false] did not match the expected credentials.
	at org.apache.shiro.realm.AuthenticatingRealm.assertCredentialsMatch(AuthenticatingRealm.java:603)
	at org.apache.shiro.realm.AuthenticatingRealm.getAuthenticationInfo(AuthenticatingRealm.java:581)
	at org.apache.shiro.authc.pam.ModularRealmAuthenticator.doSingleRealmAuthentication(ModularRealmAuthenticator.java:180)
	at org.apache.shiro.authc.pam.ModularRealmAuthenticator.doAuthenticate(ModularRealmAuthenticator.java:273)
	at org.apache.shiro.authc.AbstractAuthenticator.authenticate(AbstractAuthenticator.java:198)
	at org.apache.shiro.mgt.AuthenticatingSecurityManager.authenticate(AuthenticatingSecurityManager.java:106)
	at org.apache.shiro.mgt.DefaultSecurityManager.login(DefaultSecurityManager.java:275)
	at org.apache.shiro.subject.support.DelegatingSubject.login(DelegatingSubject.java:260)
	at org.example.TestAuthenticator.main(TestAuthenticator.java:33)

Process finished with exit code 0

我们发现,我们的用户名验证已经通过了,从报错我们可以知道是密码错了,然后发现我们登陆的密码与获取的密码不一致,将密码都改成daye,我们再试一次,如下:

false
用户名验证通过
true

Process finished with exit code 0

然后我们发现身份认证就成功了。

三.总结

第一部分我们介绍了Subject中login方法的底层实现,弄明白了该方法到底是怎么实现身份验证的,我们发现该方法底层确实是通过认证器对传入的登录信息进行认证的,而认证器又需要依赖Realm中的身份信息才可以判定用户信息是否合法,根据源码我们一步步找到了 SimpleAccountRealm中的doGetAuthenticationInfo这个方法,发现用户名的认证是在这里完成的, 密码的验证则是在AuthenticatingRealm中的assertCredentialsMatch这里完成的,并且密码的认证是无需我们参与的,Shiro会帮我们做好,所以我们仅仅需要完成用户认证返回一个带有用户信息和密码信息的SimpleAccount对象即可。根据这个思路我们设计了自己的FirstRealm我们也和SimpleAccountRealm一样去继承了AuthenticatingRealm,然后去实现了doGetAuthenticatonInfo方法,在这里去实现了用户的认证,这样我们就完成了自定义Realm的实现,我们在开发中也是这样去实现自己定义的Realm,只不过Realm的注入方式会有所不同,后续的文章,会继续介绍。

猜你喜欢

转载自blog.csdn.net/m0_46897923/article/details/114804895