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实现的简单授权功能就完成了,写文章更多的是对自己的所学所用的总结,学了用了就要掌握,不然何时才能成为大佬,不过如果能对路过的小伙伴有一丝帮助就更好了。


相关文章
|
1月前
|
Java 数据安全/隐私保护
Shiro - 授权那些事
Shiro - 授权那些事
30 0
|
存储 缓存 NoSQL
2021年你还不会Shiro?----9.Shiro利用缓存存储权限信息
前面的学习以及清楚,我们每调用一次权限的判断,就会默认调用一次自定义Realm中的doGetAuthorizationInfo方法进行过权限验证,但是当用户量与并发量比较高时,再按照这种方式去查询数据库,就会给系统带来很大的压力,让系统的响应变得很慢很,容易降低用户体验。这时候我们就需要使用缓存来存储已经登录的用户信息和用户的权限信息,缓存都是基于内存实现的比数据库快了很多,这样用户进来,直接从缓存中获取信息就行,不用去争抢有限的数据库资源了。
184 0
2021年你还不会Shiro?----9.Shiro利用缓存存储权限信息
|
存储 缓存 前端开发
2021年你还不会Shiro?----8.使用Shiro实现权限管理(前后端)
这是一个系列的文章,这是第八篇,如果只是看了这一篇或者对于Shiro没有基础的人,看到这一篇可能并不会有多大收益。前面几篇文章已经介绍了Shiro+JSP+SpringBoot+Mybatis+mysql的整合,并实现了使用MD5+盐+hash散列的方式对密码进行加密的注册登录功能。这篇是基于之前的文章进行写作的,下面就要说下登录完成后怎么实现授权操作。也就是怎么使用Shiro实现权限管理,前后端的授权是分开的,准确的说是没有关系的,所以这里也是对前后端的授权操作分开讲解。
315 0
2021年你还不会Shiro?----8.使用Shiro实现权限管理(前后端)
|
存储 缓存 安全
2021年你还不会Shiro?----2.Shiro实现登录功能(身份认证实践)
上一篇介绍了Shiro的架构,我们可以发现Shiro核心的东西并不多,我们花个几分钟就可以把Shiro的机构记清楚,其中Security Manager就是Shiro的核心,他包含了身份认证器Authenticator、授权器Authorizer、Session管理Session Manager、缓存管理Cache Manager。这一篇我们就介绍下Shiro的身份认证的过程,也就是我们说的用户登录。
116 0
2021年你还不会Shiro?----2.Shiro实现登录功能(身份认证实践)
|
存储 缓存 安全
2021年你还不会Shiro?----1.Shiro的架构
涉及到多用户登录的系统时都要对用户进行权限管理,权限管理属于系统安全的范畴,权限管理主要是为了实现“对用户访问系统的控制”,根据安全规则去控制用户可以访问的资源。
120 0
2021年你还不会Shiro?----1.Shiro的架构
|
缓存 安全 Apache
2021年你还不会Shiro?----3.分析身份认证源码实现自定义Realm
我们已经知道无论我们是认证还是授权,数据的获取都是来源于Realm,Realm就相当于我们的datasource,在上一篇中我们使用的是用IniRealm来加载我们的配置文件shiro.ini,同时我们也说了ini只是临时解决方案,在实际的开发中是不可能把用户信息和权限信息放在ini文件中的,都是来源于数据库,那么系统提供的IniRealm就不能满足我们的需要了,我们就需要自定义Realm来实现真正的场景,事实上ini文件也只是apache为我们提供学习使用的策略,下面我们就来看下怎么自己定义一个Realm。
91 0
2021年你还不会Shiro?----3.分析身份认证源码实现自定义Realm
|
存储 缓存 安全
Shiro框架01之什么是shiro+shiro的架构+权限认证
Shiro框架01之什么是shiro+shiro的架构+权限认证
Shiro框架01之什么是shiro+shiro的架构+权限认证
|
安全 数据安全/隐私保护
【Shiro】4、Shiro实现记住登录功能
用户每次在登录系统时需要重新输入账户、密码、验证码等信息,非常麻烦,于是要求加一个记住登录的功能,这对于 Shiro 来说是非常简单,下面就让我们一起来实现记住登录功能
148 0
【Shiro】4、Shiro实现记住登录功能
|
缓存 前端开发 程序员
Shiro实现多realm方案
前后端分离的背景下,在认证的实现中主要是两方面的内容,一个是用户登录获取到token,二是从请求头中拿到token并检验token的有效性和设置缓存。
Shiro实现多realm方案
|
数据库
【Shiro 系列 05】Shiro 中多 Realm 的认证策略问题
上篇文章和小伙伴们分享了 JdbcRealm,本文我想和小伙伴们聊聊多 Realm 的认证策略问题。
【Shiro 系列 05】Shiro 中多 Realm 的认证策略问题