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

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

一、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 个比特位,我们就可以存储 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 概念的参数,通过一个短短的 int 类型值传递到系统或接口里,节省空间和提高效率,拉高代码的逼格,让人看了拍案叫绝。

七、一个权限工具类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);
    }

}

Original: https://www.cnblogs.com/dijia478/p/15581941.html
Author: dijia478
Title: 关于系统权限的设计-位操作

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/593815/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • Java学习 (22) 对象篇(02)类与对象

    类与对象的关系 类是一种抽象的数据类型,它是对某一类事物整体描述/定义,但是并不能代表某一个具体的事物. 动物(猫、狗)、植物(花、草)、手机(安卓、苹果)、电脑(联想、华硕)、&…

    Java 2023年6月8日
    0102
  • 十七、JDK8 新特性(更新)

    十七、JDK8 新特性 17.1 JDK8新特性分类 前面已经学过的JDK8新特性介绍: ✅默认方法:见10.8.2 接口的语法 ✅新的日期API:见13.9.3 第三代日期类 L…

    Java 2023年6月5日
    081
  • 链表算法题解题技巧归纳总结

    最近集中刷了一批链表的题型,在这里总结一下解题技巧,以及对应题目的解题思路。 解题思路并不会细致入微,主要是为了总结归类,并且希望用几句话来激发灵感,权当是没思路时的指引以及以后复…

    Java 2023年6月5日
    064
  • maven中profiles使用详解

    使用的场景 常常遇到一些项目中多环境切换的问题。比如在开发过程中用到开发环境,在测试中使用测试环境,在生产中用生产环境的情况。springboot中提供了 spring.profi…

    Java 2023年6月7日
    0110
  • Redis内存满了怎么办(新年快乐)

    Redis内存满了怎么办(新年快乐) 入我相思门,知我相思苦。 长相思兮长相忆,短相思兮无穷极。 一、配置文件 Redis长期使用或者不设置过期时间,导致内存爆满或不足,可以到Re…

    Java 2023年6月5日
    073
  • 在 IDEA 中的各种调试技巧,轻松定位 Bug(超级全面)

    Debug用来追踪代码的运行流程,通常在程序运行过程中出现异常,启用Debug模式可以分析定位异常发生的位置,以及在运行过程中参数的变化。通常我们也可以启用Debug模式来跟踪代码…

    Java 2023年6月5日
    075
  • Sublime Text 编译 运行 Java 源代码 包 类文件

    Sublime Text 编译 Java 包 更新记录 2022/05/23 解决SublimeText控制台用户输入问题 前言 目前还存在很多问题,不过暂时能用,就先不折腾了,等…

    Java 2023年6月5日
    079
  • Windows高效开发环境配置(一)

    更多精彩内容,欢迎关注公众号:逻魔代码 前言 用了多年的 MacOS 做开发,一系列诸如 Alfred、Item2、Oh-my-zsh 之类的工具,大大地提升了工作的效率和使用舒适…

    Java 2023年6月8日
    076
  • springBoot使用注解Aop实现日志模块

    我们在日常业务操作中需要记录很多日志,可以在我们需要的方法中对日志进行保存操作,但是对业务代码入侵性大。使用切面针对控制类进行处理灵活度不高,因此我们可以使用自定义注解来针对方法进…

    Java 2023年6月8日
    098
  • Gradle连载4-依赖包打包方式

    一、apply方法的使用 apply&#xA0;<span class="hljs-string">plugin:</span>…

    Java 2023年6月13日
    0111
  • docker-compose-运行微服务项目

    1.数据库迁移 将cloud-demo涉及的相关sql导入到Linux上的mysql容器中 2.阅读docker-compose.yml文件 version: "3.2&…

    Java 2023年6月13日
    068
  • 戏说领域驱动设计(九)——架构模式

    本节开始进入DDD的战术阶段,首先要讲解的必然是DDD中的架构,毕竟程序员就喜欢这个……不过这里的架构不同于我们常说的微服务架构、单体架构、无服务架构或服务…

    Java 2023年6月7日
    085
  • websocket在线测试工具

    为了测试websocket, 根据网上的一些工具修改了一些, 因此得到了这个工具 源码 源码: websocket在线测试工具 WS WS WSS 发送 连接 清屏 断开 … …

    Java 2023年6月7日
    077
  • Java项目实战——瑞吉外卖Day07(优化篇一)

    缓存优化 问题说明 用户数量多,系统访问量大频繁访问数据库,系统性能下降,用户体验差 环境搭建 maven坐标 在项目的pom.xm1文件中导入spring data redis的…

    Java 2023年5月29日
    098
  • 4.门面Slf4j+slf4j-log4j12+log4j

    1.导入pom依赖 org.slf4j slf4j-api 1.7.27 org.slf4j slf4j-log4j12 1.7.27 log4j log4j 1.2.17 2.增…

    Java 2023年6月13日
    068
  • 【简记】virt-manager查看虚拟机详情出现报错:启动详情报错:‘NoneType’ object has no attribute ‘change_run_text’

    问题描述 因为最近某台KVM虚拟机内存不够用,准备扩容点击了virt-manager上的虚拟机详情,出现如图报错: 解决步骤 1、关闭virt-manager,重启libvirtd…

    Java 2023年5月30日
    062
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球