2021年你还不会Shiro?----5.使用Shiro实现授权功能

一.前言:

每个用户对系统的访问都会对应着身份认证,那么身份认证完了以后呢,自然就是对该用户进行授权,判断用户请求的资源是否拥有权限,或者从数据中获取该用户对应的角色,从而判断对应的资源,该用户是否可以访问。

二.授权的种类

授权的方式我们一般分为两种一种是基于角色的叫RBAC,另一种是基于资源的也叫RBAC,只不过这个R一个代表Role,一个代表Resource而已。缩写都是一样的,下面我们就来介绍下这两种授权的实现思路。

1.基于角色实现的授权

基于角色,很好理解,就是我们需要查看该用户拥有哪些角色,获取了该用户拥有的角色后,那么我们再看下哪些角色可以访问用户请求的资源,若是用户拥有的角色中恰巧有可以访问用户请求的资源的角色那么就可以访问成功,否则用户访问不成功。
那么我们看下这处代码是怎么是怎么实现的呢,在登陆后,我们就可以对用户的权限进行判断,那么我们怎么知道用户拥有哪些权限呢,这些信息同样是来源于Realm,在前面的文章我们已经说过,Realm是认证和授权的的数据来源。下面我们看下Realm中是如何实现授权数据获取的。

public class SecondRealm extends AuthorizingRealm {
    
    
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    
    
        String primaryPricncipal = (String)principalCollection.getPrimaryPrincipal();
        System.out.println("用户主体:"+primaryPricncipal);
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.addRole("admin");
        simpleAuthorizationInfo.addRoles(Arrays.asList("system","operator"));
        return simpleAuthorizationInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    
    
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken)authenticationToken;
        String userName = usernamePasswordToken.getUsername();
        //获取数据库中的用户信息
//        SimpleAccount simpleAccount = new SimpleAccount("zhaoyun","202cb962ac59075b964b07152d234b70","df",this.getName());
        SimpleAuthenticationInfo simpleAuthenticationInfo = getSimpleInfo();
        //验证用户名与token用户名是否相同
        if(simpleAuthenticationInfo.getPrincipals().asList().contains(userName)){
    
    
            return simpleAuthenticationInfo;
        }else{
    
    
            return  null;
        }
    }

    private SimpleAuthenticationInfo getSimpleInfo(){
    
    
        Md5Hash md5Hash = new Md5Hash("123");
        Md5Hash md5Hash1 = new Md5Hash("123","12345&8");
        Md5Hash md5Hash2 = new Md5Hash("123","12345&8",1024);

        return new SimpleAuthenticationInfo("zhaoyun",md5Hash2, ByteSource.Util.bytes("12345&8"),this.getName());
    }
}

就像上方的代码,我们需要重写AuthorizeingInfo方法,该方法就是用以获取当前用户的权限信息的,从代码中我们可以看出,该方法的入参是是一个PrincipalCollection,我么可以根据这个拿到该用户的primary principal,每个用户只有一个唯一的primary pricinpal,我们就可以根据这个信息去数据库查询到该身份身份信息拥有的权限信息,进而返回给登陆后的权限校验代码,这里很明显我们是基于角色实现的授权,我们通过addRole为用户设置了admin角色,然后又通过方法addRoles为用户设置了system、operator角色,当然这里的设置的角色信息我们默认是从数据库获取的这里我们只是先简单的写死去实现这个功能。那么在Realm中我们已经返回了多个的角色信息,那么我们再用户进入资源时应该怎么写呢,请看下方代码

public class TestAuthenticator2 {
    
    
    public static void main(String[] args) throws Exception{
    
    
        //定义安全管理器
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        //定义一个支持MD5+盐+hash散列的密码匹配器
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        //告诉密码匹配器密文是哪种加密方式
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        //告诉密码匹配器,密码被散列的次数,这个一般定义为1024的倍数,默认一次
        hashedCredentialsMatcher.setHashIterations(1024);
        //定义自己的realm
        SecondRealm secondRealm = new SecondRealm();
        //为realm设置密码匹配器
        secondRealm.setCredentialsMatcher(hashedCredentialsMatcher);
        //为安全管理器设置realm
        defaultSecurityManager.setRealm(secondRealm);
        //模拟用户登录场景
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("zhaoyun","123");
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        try {
    
    
            Subject subject = SecurityUtils.getSubject();
            subject.login(usernamePasswordToken);
            System.out.println("登录成功");


            if(subject.isAuthenticated()){
    
    
                //认证通过
                if(subject.hasRole("admin")){
    
    
                    System.out.println("该用户拥有admin的权限");
                }else{
    
    
                    System.out.println("该用户没有admin的权限");
                }
            }
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }
}

如上方代码所示,我们先判断,认证是否通过,当认证通过后,我们就可以对用户角色进行判断,当用户拥有该角色时就可以进入用户请求的资源,否则进入失败,根绝上方两处代码,我们可以知道,该用户是拥有admin该角色的,所以正常情况下输出应该是:该用户拥有admin的权限。那么是不是这样呢,运行程序输出如下:

登录成功
用户主体:zhaoyun
该用户拥有admin的权限

Process finished with exit code 0

从上方的输出结果我们可以看出,此次授权已经成功,用户请求的资源要求用户拥有admin权限,而用户通过Realm获取到的用户拥有的权限信息也拥有admin那么用户就可以完成访问,由此一次请求也就顺利完成了,整个代码也就像上方那样,只不过是我们的权限信息来源并非数据库而已,不过连接数据这块毕竟对于程序员并不难,所以这里再稍后的几篇文章会进行整合,这里我们先只是假定数据来源于数据库。到这里基于角色实现的权限管理的demo就完成了,我们可以看到我们需要在Realm中设置角色信息,再登陆后判断用户是否拥有角色,这样就可以了。

2.基于资源实现的授权

上面我们已经看到,在基于角色实现授权时,我们需要为用户绑定角色信息,然后通过Realm来获取每个用户的角色,从而判定用户是否对相应的资源是否拥有访问权限。同样的如果我们基于资源实现授权,我们就要为每个用户绑定该用户可以访问的资源信息,当然这个信息也是存在数据库中的,我们需要通过Realm来获取到每个用户拥有的资源信息,拿到该用户所拥有的所有资源信息后,再与用户当前请求的资源比对,包含请求的资源则可以访问成功,否则授权失败,返回一个设定的路径给该用户。下面我们看下,基于角色的授权Realm应该怎么写。

public class SecondRealm extends AuthorizingRealm {
    
    
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    
    
        String primaryPricncipal = (String)principalCollection.getPrimaryPrincipal();
        System.out.println("用户主体:"+primaryPricncipal);
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.addStringPermission("system:supplier:update");
        simpleAuthorizationInfo.addStringPermissions(Arrays.asList("system:*:view","system:supplier:delete"));
        return simpleAuthorizationInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    
    
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken)authenticationToken;
        String userName = usernamePasswordToken.getUsername();
        //获取数据库中的用户信息
//        SimpleAccount simpleAccount = new SimpleAccount("zhaoyun","202cb962ac59075b964b07152d234b70","df",this.getName());
        SimpleAuthenticationInfo simpleAuthenticationInfo = getSimpleInfo();
        //验证用户名与token用户名是否相同
        if(simpleAuthenticationInfo.getPrincipals().asList().contains(userName)){
    
    
            return simpleAuthenticationInfo;
        }else{
    
    
            return  null;
        }
    }

    private SimpleAuthenticationInfo getSimpleInfo(){
    
    
        Md5Hash md5Hash = new Md5Hash("123");
        Md5Hash md5Hash1 = new Md5Hash("123","12345&8");
        Md5Hash md5Hash2 = new Md5Hash("123","12345&8",1024);

        return new SimpleAuthenticationInfo("zhaoyun",md5Hash2, ByteSource.Util.bytes("12345&8"),this.getName());
    }
}

从上方代码我们可以看到, 我们是使用addStringPermission和addStringPermissions两个方法来为用户设置资源权限的。基于角色实现时则是通过addRole和addRoles两个方法,那么在控制用户访问时要怎么判断呢?我们继续来看下面的代码

public class TestAuthenticator2 {
    
    
    public static void main(String[] args) throws Exception{
    
    
        //定义安全管理器
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        //定义一个支持MD5+盐+hash散列的密码匹配器
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        //告诉密码匹配器密文是哪种加密方式
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        //告诉密码匹配器,密码被散列的次数,这个一般定义为1024的倍数,默认一次
        hashedCredentialsMatcher.setHashIterations(1024);
        //定义自己的realm
        SecondRealm secondRealm = new SecondRealm();
        //为realm设置密码匹配器
        secondRealm.setCredentialsMatcher(hashedCredentialsMatcher);
        //为安全管理器设置realm
        defaultSecurityManager.setRealm(secondRealm);
        //模拟用户登录场景
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("zhaoyun","123");
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        try {
    
    
            Subject subject = SecurityUtils.getSubject();
            subject.login(usernamePasswordToken);
            System.out.println("登录成功");

            if(subject.isAuthenticated()){
    
    
                //认证通过
                if(subject.isPermitted("system:view:supplier")){
    
    
                    System.out.println("该用户拥有system:supplier:view的权限");
                }else{
    
    
                    System.out.println("该用户没有system:supplier:view的权限");
                }
            }
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }
}

在基于角色实现的权限管理时,我们使用的是hasRole,在这里基于资源时我们则是使用的isPermitted来判断对相应资源是否拥有访问权限。拥有则可以进入相应的资源,不然则访问不了对应的资源。

三.基于资源的授权解读

如果使用Shiro做过多个项目的朋友一定见过这两种不同的资源定义的字符串

  1. system:suppliermessage:view 用户:资源:权限
  2. system:view:suppliermessage 用户:权限:资源

有的人就会产生疑问(我也是这样),这两种是不是有一种写错了?其实不然,我们可以看到这两种权限字符串后面两个书写是反着来的,其实两种书写都是对的,就算将system的位置调整到中间、最后也是可以的。并没有实质上的影响,我们只要明白了Shiro在基于资源实现的授权模式中是如何判定的就不会有这样的疑问了,下面我们基于两种情况解读下。

1.假设场景一

假设用户请求的资源是system:suppliermessage:view,该用户拥有的资源字符串包含这个system:suppliermessage:view,很明显用户可以访问成功。我们做个验证代码如下:

public class TestAuthenticator3 {
    
    
    public static void main(String[] args) {
    
    
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        ThreeRealm threeRealm = new ThreeRealm();
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher("md5");
        hashedCredentialsMatcher.setHashIterations(1024);
        threeRealm.setCredentialsMatcher(hashedCredentialsMatcher);
        defaultSecurityManager.setRealm(threeRealm);
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("luban","PANcc1232!@#");
        subject.login(usernamePasswordToken);

        System.out.println("认证通过了");

        if(subject.isAuthenticated()){
    
    
            //认证通过
            if(subject.isPermitted("system:suppliermessage:view")){
    
    
                System.out.println("该用户可以访问该资源");
            }else{
    
    
                System.out.println("该用户不可以访问该资源");
            }
        }

    }
}

上面是模拟的登录验证场景,下面是自定义的Realm的代码:

public class ThreeRealm extends AuthorizingRealm {
    
    
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    
    
        String userName = (String)principalCollection.getPrimaryPrincipal();
        if(userName.equals("luban")){
    
    
            //假设该数据来源于数据库
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            simpleAuthorizationInfo.addStringPermission("system:suppliermessage:view");
            simpleAuthorizationInfo.addStringPermissions(Arrays.asList("system:suppliermessage:add","system:suppliermessage:delete"));
            return simpleAuthorizationInfo;
        }

        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    
    
        String userName = (String) authenticationToken.getPrincipal();
        if(userName.equals("luban")){
    
    
            //这里代表去数据库获取到了数据
            Md5Hash md5Hash = new Md5Hash("PANcc1232!@#","123asd",1024);
            SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo("luban",md5Hash, ByteSource.Util.bytes("123asd"),this.getName());
            return simpleAuthenticationInfo;
        }
        return null;
    }
}

执行结果如下:

认证通过了
该用户可以访问该资源

Process finished with exit code 0

我们可以看到当对资源的操作放在了资源字符串第三位时是可以正常授权的,所以说使用这种字符串资源路径没有问题。那么放在第二位呢。

2.假设场景二

这里假设用户请求的资源是system:view:suppliermessage这么写的,该用户拥有的资源字符串是这个system:view:suppliermessage,那这样能授权成功吗,代码如下:

public class TestAuthenticator3 {
    
    
    public static void main(String[] args) {
    
    
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        ThreeRealm threeRealm = new ThreeRealm();
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher("md5");
        hashedCredentialsMatcher.setHashIterations(1024);
        threeRealm.setCredentialsMatcher(hashedCredentialsMatcher);
        defaultSecurityManager.setRealm(threeRealm);
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("luban","PANcc1232!@#");
        subject.login(usernamePasswordToken);

        System.out.println("认证通过了");

        if(subject.isAuthenticated()){
    
    
            //认证通过
            if(subject.isPermitted("system:view:suppliermessge")){
    
    
                System.out.println("该用户可以访问该资源");
            }else{
    
    
                System.out.println("该用户不可以访问该资源");
            }
        }

    }
}

以上是登陆授权的代码,下面则是自定义的Realm。

public class ThreeRealm extends AuthorizingRealm {
    
    
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    
    
        String userName = (String)principalCollection.getPrimaryPrincipal();
        if(userName.equals("luban")){
    
    
            //假设该数据来源于数据库
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            simpleAuthorizationInfo.addStringPermission("system:view:suppliermessge");
            simpleAuthorizationInfo.addStringPermissions(Arrays.asList("system:add:suppliermessge","system:delete:suppliermessge"));
            return simpleAuthorizationInfo;
        }

        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    
    
        String userName = (String) authenticationToken.getPrincipal();
        if(userName.equals("luban")){
    
    
            //这里代表去数据库获取到了数据
            Md5Hash md5Hash = new Md5Hash("PANcc1232!@#","123asd",1024);
            SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo("luban",md5Hash, ByteSource.Util.bytes("123asd"),this.getName());
            return simpleAuthenticationInfo;
        }
        return null;
    }
}

执行结果如下:

认证通过了
该用户可以访问该资源

Process finished with exit code 0

我们会发现确实如我们所预料的那样,资源权限字符串其实是可以随意放的,但是随意放,我们就需要做好相应位置的权限管理,不可以一部分操作在第三位,一部分在第二位,一个项目中最少是必须统一的,其实Shiro在进行判断时,就是根据三个位置上的包含关系进行判断只要每个位置都满足了包含关系,那么就拥有权限,不论你将资源名或者操作名放在第几位,这个其实时没有影响的。

四.总结

这篇文章里主要介绍了Shiro对授权的两种实现方式,其实无论是Shiro或者是SpringSecurity都是支持这两种授权方式的,也就是基于角色实现授权与基于资源实现授权。基于角色时需要为返回的SimpleAuthorizatingInfo增加角色使用addRole和addRoles方法实现,授权验证时则是判断subject是否拥有某角色,使用方法hasRole来判断。基于资源时需要为返回的SimpleAuthorizatingInfo增加资源字符串使用方法addStringPermisson和addStringPermissions来实现,授权验证时则是判断subject是否被相应的资源路径允许,使用isPermitted来判断。在介绍完了这两种不同的授权实现后,则是又分析了在不同项目中碰到过的不同资源字符串的格式,到此使用Shiro实现的简单授权功能就完成了,写文章更多的是对自己的所学所用的总结,学了用了就要掌握,不然何时才能成为大佬,不过如果能对路过的小伙伴有一丝帮助就更好了。

猜你喜欢

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