• 关于系统权限的设计位操作


    本文讨论是权限设计的其中一种思路,有它自己的优缺点,不一定适用于所有系统。

    一、Linux文件权限

    大家都知道,Linux上有三种文件权限:

    • r:表示读取,对应的数字为 4;
    • w:表示写入,对应的数字为 2;
    • x:表示执行,对应的数字为 1;

    当然还有一种是特殊的是:

    • -:表示无权限,对应的数字为0;

    通过这四个数字以及他们的组合,就可以表示任意一种权限:

    1+2=3:有执行、写入权限,没有读取权限。

    1+4=5:有执行、读取权限,没有写入权限。

    2+4=6:有写入、读取权限,没有执行权限。

    1+2+4=7:有执行、写入、读取权限。

    不知道大家有没有想过,为什么权限的标识要是0/1/2/4这几个数字呢?为什么不是0/1/2/3?为什么不是6/7/8/9?为什么0/1/2/4组合就可以表示所有权限呢?

    有些人会解释说,如果用0/1/2/3,那3表示的是3本身呢?还是1+2呢?意义不明确呀。用其他的数字又太大,不方便计算,没有0124简单。其实这些都不是真正的原因,Linux文件权限设计之所以要用0124,是因为它其实是用二进制表示权限的。

    二、用二进制比特位表示权限

    二进制是什么想必不用我多说,我们都知道二进制表示的数字,只有0和1两种数。那么我们就想,如果我用0表示没有权限,用1表示有权限,这样岂不是很简单?

    我继续用Linux文件权限举例。

    用第一个比特位表示是否有执行权限,第二个比特位表示是否有写入权限,第三个比特位表示是否有读取权限。每个比特位的0表示没有权限,1表示有权限。(后文所说的第几位,都是指从右向左数)

    那么我们试一试,如何表示只有写入权限,没有读取和执行权限呢?

    按照上面的规则,应该是0010对吧,将这个二进制数字转成十进制数字,刚好是2。以此类推,就有了0/1/2/4这四个数字。

    分别表示:无权限(0000)、执行(0001)、写入(0010)、读取(0100)

    再验证一下,如何表示有执行、读取权限,没有写入权限呢?应该是0101,将这个二进制数字转成十进制数字,刚好是5,符合我们上面说的。

    所以,只要我们提前约定好每个比特位代表的是什么权限,我们就可以通过一个二进制数字,表示大量的权限及其自由组合,而且非常的节省存储空间,1个字节,我们就可以存储8种权限。在Java语言中的int类型有4个字节,一个int值,就可以存储32种权限,以及2的32次方种权限组合。那么知道了这个知识点,我们如何将它设计进我们的权限系统呢?

    三、权限的增删查

    知道了如何存储权限,但是我们还需要操作权限。而一个权限系统,必然会有三大基础操作(因为权限非有即无,所以编辑操作等价于添加或删除):

    • 添加一个权限
    • 删除一个权限
    • 以及校验一个权限

    那我们如何对一个二进制数字表示的权限,进行增删查呢?这里就要利用位操作了:

    • | 添加权限
    • ^ 删除权限(已有权限时)
    • & 校验权限

    用代码来解释一下:

        /**
         * 添加权限
         *
         * @param currentPermission 原权限
         * @param permissions       需要添加的权限集合
         * @return 添加完权限后的十进制数字
         */
        public static int addPermissions(int currentPermission, int... permissions) {
            for (int permission : permissions) {
                currentPermission |= permission;
            }
            return currentPermission;
        }
    
        /**
         * 从已有权限里,删除权限
         *
         * @param currentPermission 原已有权限
         * @param permissions       需要删除的权限集合
         * @return 删除完权限后的十进制数字
         */
        public static int removePermissions(int currentPermission, int... permissions) {
            for (int permission : permissions) {
                currentPermission ^= permission;
            }
            return currentPermission;
        }
    
        /**
         * 校验权限
         *
         * @param currentPermission 原权限
         * @param permission        要校验的权限
         * @return 是否含有权限
         */
        public static boolean hasPermission(int currentPermission, int permission) {
            return (currentPermission & permission) == permission;
        }
    

    为什么上述三个操作可以做到添加、删除、查询权限呢?我们需要来复习一下位运算。

    位运算
    或(|):两个都为0,结果才为0,否则为1 0 | 0 = 0 0 | 1 = 1 1 | 0 = 1 1 | 1 = 1
    异或(^):两个相同为0,不相同为1 0 ^ 0 = 0 0 ^ 1 = 1 1 ^ 0 = 1 1 ^ 1 = 0
    与(&):两个都为1,结果才为1,否则为0 0 & 0 = 0 0 & 1 = 0 1 & 0 = 0 1 & 1 = 1
    取反(~):0变1,1变0 ~0 = 1 ~1 = 0

    根据位运算的特点,我们可以发现:

    • 给执行权限里,添加写入权限,本质应该是将执行权限(0001)的第二个0,变成1,那么只要或(|)一个写入权限(0010)就好了。(或一个数,就代表将这个数表示的权限,添加到原来的数字里。无论原来的数字有没有权限都会加进去):

    • 在执行、写入权限里,删除写入权限,本质应该是将执行、写入权限(0011)的第二个1,变成0,那么只要异或(^)一个写入权限(0010)就好了。(异或一个数,就代表将这个数表示的权限,从原来的数字里删除。只有在原来的数字已有这个权限时才会删除,否则是添加):

    • 在执行、写入权限里,判断是否有写入权限,其本质应该是判断执行、写入权限(0011)的第二位,是否是1,那么只要与(&)一个写入权限(0010),再将结果和写入权限(0010)自身比较一下是否相等就好了。(与一个数,如果还等于这个数,就代表原来的数字有这个数表示的权限):

    这里特别说明一下“异或”删除权限的操作,只有原来的数字里已经有该权限了,才可以删除。从异或运算的规则中可以发现,异或运算其实是无则增,有则减的操作。那么如果我想,无论原来的数字有没有权限,都删除权限(无论原来是0或1,都变成0),该怎么操作呢?

    有三个方法:

    第一个方法是,异或操作前,先判断下有无权限,有权限时再删除,无权限自然也不需要删除权限。

        /**
         * 从已有权限里,删除权限
         *
         * @param currentPermission 原已有权限
         * @param permissions       需要删除的权限集合
         * @return 删除完权限后的十进制数字
         */
        public static int removePermissions1(int currentPermission, int... permissions) {
            for (int permission : permissions) {
                if (!hasPermission(currentPermission, permission)) {
                    continue;
                }
                currentPermission ^= permission;
            }
            return currentPermission;
        }
    

    第二个方法是,利用添加权限时,无论有没有权限都会加上去的特点,我们可以先添加,再删除。

        /**
         * 删除权限
         *
         * @param currentPermission 原权限
         * @param permissions       需要删除的权限集合
         * @return 删除完权限后的十进制数字
         */
        public static int removePermissions2(int currentPermission, int... permissions) {
            for (int permission : permissions) {
                currentPermission = addPermissions(currentPermission, permission);
                currentPermission ^= permission;
            }
            return currentPermission;
        }
    

    第三个方法是,先“取反”运算,再“与”运算

        /**
         * 删除权限
         *
         * @param currentPermission 原权限
         * @param permissions       需要删除的权限集合
         * @return 删除完权限后的十进制数字
         */
        public static int removePermissions3(int currentPermission, int... permissions) {
            for (int permission : permissions) {
                currentPermission &= ~permission;
            }
            return currentPermission;
        }
    

    第三种这个理解起来比较困难,我解释一下。还是以在执行、写入权限里,删除写入权限为例,先对要删除的写入权限(0010)“取反”

    再将执行、写入权限(0011)“与”一个上面取反的结果

    可以发现,结果是一样的。这样的好处就是,不用判断原来的数字里,是否含有权限了,也不用先改变原来数字的权限,但没有第一种和第二种方法那么简单易懂。

    这里推荐使用第三种方法,因为前两种在某些业务场景下会有并发问题,是线程不安全的(若是如代码示例那样,方法参数是基本类型,而非引用类型,那就不会有线程安全问题了,这三种方法都可以)。

    四、总结

    优点:既然是二进制存储,位运算操作,肯定有节省空间,效率极高的优点,当然同时也是逼格满满。

    缺点:权限种类有限,如果用int存储,最多只能有32种权限类型,用long型则最多有64种权限类型。所以本文的这种设计最好还是用于权限种类不太多的情况比较好。

    举一反三,本文的知识点也不仅仅用于权限系统。

    例如你写了个接口,提供了大量可选操作,比如校验参数,记录日志,使用缓存,发送通知等等,如果有十几个这种选项,你会怎么设计你的接口呢?是通过一个对象封装,把每个选项都设计成一个boolean类型字段,然后在接口里到处判断字段的值是true还是false吗?这样固然可以实现,但就是显得有点普通平庸了,体现不出来你的水平。

    而通过本文的方法,就可以将众多表达true/false概念的参数,通过一个短短的数字传递到系统或接口里,节省空间和提高效率,拉高代码的逼格,让人看了拍案叫绝。

    五、一个权限工具类demo

    /**
     * 权限工具类demo
     *
     * @author dijia478
     * @date 2021-11-20 16:52:10
     */
    public class PermissionUtils {
    
        /**
         * 所有权限都没有
         */
        public static final int NOT_ALL = 0;
    
        /**
         * 所有权限都有
         */
        public static final int HAVE_ALL = -1;
    
        /**
         * 执行权限
         */
        public static final int EXECUTE = 1 << 0;
    
        /**
         * 新建权限
         */
        public static final int CREATE = 1 << 1;
    
        /**
         * 查询权限
         */
        public static final int SELECT = 1 << 2;
    
        /**
         * 修改权限
         */
        public static final int UPDATE = 1 << 3;
    
        /**
         * 删除权限
         */
        public static final int DELETE = 1 << 4;
    
        /**
         * 进行字段校验
         */
        public static final int CHECK_ITEM = 1 << 5;
    
        /**
         * 记录操作日志
         */
        public static final int OPERATE_LOG = 1 << 6;
    
        /**
         * 发送通知
         */
        public static final int SEND_MSG = 1 << 7;
    
        /**
         * 使用缓存
         */
        public static final int USE_CACHE = 1 << 8;
    
        /**
         * 添加权限
         *
         * @param currentPermission 原权限
         * @param permissions       需要添加的权限集合
         * @return 添加完权限后的十进制数字
         */
        public static int addPermissions(int currentPermission, int... permissions) {
            for (int permission : permissions) {
                currentPermission |= permission;
            }
            return currentPermission;
        }
    
        /**
         * 从已有权限里,删除权限
         *
         * @param currentPermission 原已有权限
         * @param permissions       需要删除的权限集合
         * @return 删除完权限后的十进制数字
         */
        public static int removePermissions1(int currentPermission, int... permissions) {
            for (int permission : permissions) {
                if (!hasPermission(currentPermission, permission)) {
                    continue;
                }
                currentPermission ^= permission;
            }
            return currentPermission;
        }
    
        /**
         * 删除权限
         *
         * @param currentPermission 原权限
         * @param permissions       需要删除的权限集合
         * @return 删除完权限后的十进制数字
         */
        public static int removePermissions2(int currentPermission, int... permissions) {
            for (int permission : permissions) {
                currentPermission = addPermissions(currentPermission, permission);
                currentPermission ^= permission;
            }
            return currentPermission;
        }
    
        /**
         * 删除权限
         *
         * @param currentPermission 原权限
         * @param permissions       需要删除的权限集合
         * @return 删除完权限后的十进制数字
         */
        public static int removePermissions3(int currentPermission, int... permissions) {
            for (int permission : permissions) {
                currentPermission &= ~permission;
            }
            return currentPermission;
        }
    
        /**
         * 校验权限
         *
         * @param currentPermission 原权限
         * @param permission        要校验的权限
         * @return 是否含有权限
         */
        public static boolean hasPermission(int currentPermission, int permission) {
            return (currentPermission & permission) == permission;
        }
    
        /**
         * 获取所有权限都有
         *
         * @return 拥有所有权限的十进制数字,其实就是-1
         */
        public static int getAllPermission() {
            return HAVE_ALL;
        }
    
        /**
         * 获取所有权限都没有
         *
         * @return 没有所有权限的十进制数字,其实就是0
         */
        public static int getNotAllPermission() {
            return NOT_ALL;
        }
    
        /**
         * 只获取需要的权限,除了需要的,其他权限都没有
         *
         * @param permissions 需要的权限
         * @return 所有需要的权限的十进制数字
         */
        public static int getNeedPermission(int... permissions) {
            return addPermissions(NOT_ALL, permissions);
        }
    
        /**
         * 排除不需要的权限,除了不需要的,其他权限都有
         *
         * @param permissions 不需要的权限
         * @return 所有不需要的权限的十进制数字
         */
        public static int getNotNeedPermission(int... permissions) {
            return removePermissions3(HAVE_ALL, permissions);
        }
    
    }
    
    © 版权声明
    文章版权归作者所有,欢迎转载,但必须给出原文链接,否则保留追究法律责任的权利
    THE END
  • 相关阅读:
    投行风云:FO的酸甜苦辣【转】
    周末排毒
    LeetCode | Single Number II【转】
    Adding supplementary tables and figures in LaTeX【转】
    Algorithm | Tree traversal
    Leetcode | Linked List Cycle I && II
    Network | UDP checksum
    Ubuntu下将现有的文件打包成deb包
    Base64编码加密
    requestAnimationFrame 的原理与优势
  • 原文地址:https://www.cnblogs.com/dijia478/p/15581941.html
Copyright © 2020-2023  润新知