【重构思维】用位运算做权限管理

张开发
2026/5/18 16:37:30 15 分钟阅读
【重构思维】用位运算做权限管理
一、从一个具体的问题说起假设你在开发一个内容管理系统需要控制用户对文章的操作权限。有的用户只能看有的能看也能写还有的管理员可以删除文章。这是每个后台系统都会遇到的基础需求。按照常规思路我们可能会用一个对象来记录用户的各项权限constuserPermissions{canRead:true,canWrite:false,canDelete:false};这种做法很直观但如果我们有十几个甚至几十个权限呢对象会越来越臃肿每次判断权限都要访问对象的属性还要处理各种默认值。更麻烦的是当我们需要把权限信息存到本地存储或者通过网络传给后端时这个对象的体积会比较大。这时候我们可以换一个角度思考计算机最底层处理的是什么是数字是二进制的零和一。一个整数在内存中就是一串二进制位我们能不能用这一串位来编码所有的权限信息呢这就是位运算权限系统的核心出发点。二、用二进制位编码权限的基本思想要理解位运算权限系统首先得明白整数在计算机中是怎么存储的。我们知道计算机内部用二进制表示所有数据。一个整数比如数字五用三十二位二进制表示写出来是这样的00000000 00000000 00000000 00000101从右往左看第一位是1第二位是0第三位是1其余位都是0。这串二进制数本质上就是一个由零和一组成的序列。那么如果我们做一个约定让这个序列的每一位都对应一个权限这一位是1就表示有这个权限是0就表示没有这样不就能用一个整数编码所有权限了吗具体怎么对应呢我们可以从最右边开始编号第0位最右边值为1表示可读第1位值为2表示可写第2位值为4表示可删除按照这个规则如果某个用户的权限值是1二进制是001表示只有读权限。如果是3二进制是011表示有读和写权限。如果是7二进制是111表示三个权限都有。这样一来原本需要用对象或数组存储的权限信息现在被压缩到了一个整数里。这不仅节省了存储空间更重要的是我们为后续的高速运算创造了条件。因为位运算在CPU层面是直接支持的执行速度极快。三、如何计算每个权限对应的数值明白了用二进制位编码权限的思想接下来的问题是在实际代码中我们如何方便地定义每个权限对应的数值呢我们知道每个权限对应一个特定的二进制位这个位的位置决定了它的数值。第0位是1第1位是2第2位是4第3位是8以此类推。这些数值都是2的整数次幂。在JavaScript中有一种专门用来处理二进制位的运算符叫做左移运算符用两个小于号表示。它的作用是把一个数的二进制位整体向左移动指定的位数右边空出的位置补零。1 0 结果是1二进制是1 1 1 结果是2二进制是10 1 2 结果是4二进制是100 1 3 结果是8二进制是1000可以看到1 n的结果就是2的n次方其二进制形式恰好是在第n位从0开始计数上为1其他位都是0。这正好就是我们需要的权限掩码。利用这个特性我们可以非常清晰地定义权限常量constPermission{READ:10,// 值为1二进制第0位是1WRITE:11,// 值为2二进制第1位是1DELETE:12,// 值为4二进制第2位是1ADMIN:13// 值为8二进制第3位是1};这种写法有几个好处。首先意图非常明确一眼就能看出每个权限对应哪一位。其次方便调整如果需要修改某个权限对应的位只需要改一下左移的位数就行。最后便于扩展增加新的权限只需要继续往下写1 4、1 5即可。需要注意的是这种方案有一个隐含的限制由于JavaScript的位运算在内部会把操作数转换为32位有符号整数而且最高位是符号位所以实际可用的只有31位。也就是说这种方案最多支持31个不同的权限。对于大多数业务系统来说31个权限已经足够。如果确实需要更多可以考虑使用BigInt或者分块存储的方案这里先不展开。四、添加权限按位或运算定义好了权限常量接下来的核心问题是如何给用户添加权限假设我们有一个变量userPermission用来存储用户的权限值初始状态是0表示没有任何权限。现在我们要给他添加读权限。如果只是简单地赋值会覆盖掉之前的状态。所以我们需要一种运算能够在不影响其他位的前提下把某一位设置成1。这个运算就是按位或用竖线符号|表示。按位或的规则是两个位中只要有一个是1结果就是1只有两个都是0结果才是0。0101 (5) | 0011 (3) ---- 0111 (7)把这个规则应用到权限添加上原理是这样的权限掩码比如Permission.READ只有它对应的那一位是1其他位都是0。当它和用户的权限值进行或运算时那位如果原来是0就会变成1如果原来已经是1则保持1不变而其他位因为和0进行或运算所以保持原状。具体到代码letuserPermission0;// 初始没有任何权限// 添加读权限userPermission|Permission.READ;// 现在 userPermission 的值是 1 (二进制 001)// 再添加写权限userPermission|Permission.WRITE;// 现在 userPermission 的值是 3 (二进制 011)无论用户之前有没有某个权限用或运算添加都是安全的。如果之前没有对应位原来是0或上1之后变成1如果之前已经有了对应位已经是1或上1之后还是1不会发生变化。如果要一次性添加多个权限可以先把这些权限用或运算组合在一起然后再和用户的权限值进行或运算// 一次性添加读、写、删除三个权限userPermission|Permission.READ|Permission.WRITE|Permission.DELETE;这里的Permission.READ | Permission.WRITE | Permission.DELETE先把三个权限掩码通过或运算合并成一个值然后再和userPermission进行或运算实现批量添加。这种写法既简洁又高效。五、检查权限按位与运算添加权限解决了接下来的核心需求是如何检查用户有没有某个权限。这是权限系统中调用最频繁的操作必须保证足够快。按位与运算正好能满足这个需求。按位与用和号符号表示规则是两个位都是1结果才是1只要有一个是0结果就是0。0101 (5) 0011 (3) ---- 0001 (1)检查权限的思路是这样的把用户的权限值和要检查的权限掩码进行与运算。如果结果不为零说明用户有这个权限如果结果为零说明没有。原理在于权限掩码只有它对应的那一位是1其他位都是0。进行与运算后如果用户权限的那一位是1结果就是那个掩码值非零如果用户权限的那一位是0结果就一定是0。代码实现// 检查是否有读权限consthasRead(userPermissionPermission.READ)!0;// 检查是否有写权限consthasWrite(userPermissionPermission.WRITE)!0;这里要注意判断条件。因为与运算的结果可能是任何非零值不一定是1所以我们只需要判断结果是否不等于零即可。千万不要用 Permission.READ这种判断除非你很确定用户的权限值只有这一位是1。举个例子说明。假设用户权限值是5二进制101表示有读和删除权限没有写权限。检查读权限101 (5用户权限) 001 (1读权限掩码) --- 001 (1非零说明有读权限)检查写权限101 (5) 010 (2写权限掩码) --- 000 (0为零说明没有写权限)结果一目了然。这种检查方式的时间复杂度是O(1)无论系统有多少种权限校验一次都只需要进行一次位运算速度极快。实际项目中通常会封装成一个函数或方法functionhasPermission(userPerm,checkPerm){return(userPermcheckPerm)!0;}// 使用if(hasPermission(userPermission,Permission.WRITE)){console.log(可以编辑文章);}检查多个权限中的任意一个可以先把这些权限用或运算组合起来再和用户的权限进行与运算// 检查是否有读或写的权限constcanAccess(userPermission(Permission.READ|Permission.WRITE))!0;检查是否同时拥有多个权限要确保与运算的结果等于这些权限的组合值// 检查是否同时有读、写、删除三个权限constrequiredPermission.READ|Permission.WRITE|Permission.DELETE;consthasAll(userPermissionrequired)required;这里的区别在于检查任意一个用! 0检查同时拥有用 required。前者只要有重合就行后者要求完全覆盖。六、移除权限按位与和按位非的配合说完了添加和检查再来看最后一个核心操作如何移除权限。移除权限的需求很常见。比如管理员临时收回某个用户的某项操作权或者用户自己关闭了某个功能模块。我们需要一种运算能够把某一位从1变回0同时不影响其他位。单独使用按位与或按位或都无法直接实现这个需求。因为按位或只能把0变成1不能把1变成0按位与只能保留某些位但不太好单独清除某一位。这时候就需要按位非运算出场了。按位非用波浪线符号~表示作用是把一个数的所有二进制位取反0变成11变成0。~ 0101 (5) ---- 1010 (-6补码表示)按位非很少单独使用它通常是和按位与配合起来实现清除某一位的功能。具体做法是先对要清除的权限掩码取反这样原来那位是1的位置变成了0其他位都变成了1然后用这个结果和原权限值进行按位与运算。代码实现// 移除写权限userPermission~Permission.WRITE;原理分解一下。假设Permission.WRITE是2二进制是010。对它取反得到...101前面还有很多1。然后和userPermission进行与运算那位0就会把userPermission对应位强制变成0而那些1会让其他位保持原状。举个例子。用户原本权限值是7二进制111拥有读、写、删除三个权限现在我们要移除写权限值是2二进制010。首先对写权限掩码取反~ 010 (2写权限掩码) --- ...11111101 (所有位取反低位是101)然后和原权限值进行与运算111 (7原权限) ...101 (取反后的掩码) --- 101 (5只剩下读和删除权限)结果变成了5二进制101写权限被成功移除了其他权限不受影响。如果要一次性移除多个权限可以先把这些权限用或运算组合然后取反再进行与运算// 同时移除写和删除权限userPermission~(Permission.WRITE|Permission.DELETE);这种写法先Permission.WRITE | Permission.DELETE把两个掩码合并然后~取反最后清除对应的位。简洁而高效。七、总结通过这篇文章我们一步步构建起了这套高效、紧凑的权限管理方案。回顾整个过程核心要点可以归纳为以下几点第一位运算权限系统的本质是用一个整数的二进制位来编码权限状态。每个权限对应一个特定的位这一位是1就表示拥有该权限是0则表示没有。这样一个整数就能承载所有权限信息。第二定义权限时使用左移运算1 n来生成掩码。这既保证了每个权限对应唯一的二进制位又让代码清晰可读便于维护和扩展。第三添加权限用按位或运算|检查权限用按位与运算移除权限则需要按位非和按位与的配合 ~。这四种位运算组合起来覆盖了权限管理的全部核心操作。第四这种方案的最大优势在于性能。添加、移除、检查权限的时间复杂度都是O(1)与权限总数无关。同时存储极其紧凑序列化后就是一个数字网络传输和本地存储都非常高效。当然位运算权限系统也有其适用边界。它最适合权限数量相对较少几十个以内、权限标识固定的场景。如果权限需要动态增删或者数量可能爆发式增长就需要考虑分块存储或BigInt等进阶方案。

更多文章