通过JS位运算实现权限控制

92 阅读6分钟

一、前言

位运算, 平常工作中用的比较少, 但是不代表不重要, 像 React 源码中的 Lane 模型就涉及到位运算的内容, 很多涉及到此的学习资料,通常讲的一笔带过, 经常不知道是为什么, 今天我们就通过设计一个小的权限控制系统, 学习一下位运算的基本使用

二、基础知识

1、 为什么 0.1 + 0.2 算不准?

面试经常问 0.1 + 0.2 为什么算不准, 怎么解决 其实这个问题就和我们今天的学习有关, 0.1 和 0.2 我们都知道是十进制的浮点数,转化为二进制时是无限循环的小数, 而JS使用的IEEE 745标准下, 二进制数字是有范围的 在 -(2^53 -1) 至 2^53 -1 之间,所以超出的部分会丢失, 所以在计算后, 0.1+0.2 !== 0.3, 解决方式有很多种 如:

    1. tofixed : (0.1+0.2).toFixed(1) 通过四舍五入修正
    1. 浮点数转成整数计算后算回: (0.110+0.210)/10
    1. 通过转成字符串然后经过一定规则计算,一些库就是这样实现的
    1. 使用 math.js 库等

image.png

2、学一下进制的表示

我们经常使用十进制, 对其他进制用的比较少,不同进制之间的计算通常由JS内部转化处理, 然后输出十进制,接下来看一下不同进制的表示

// 十进制
123456 
// 二进制 0b 0B开头
0b1    //1
// 八进制 0o 0O
0o755 // 493
// 十六进制 color 颜色值的表示经常使用
0xA  // 10

toString 方法默认转化成10进制, 内部可以通过数字转化为其他进制

let binary = (1).toString(2);
console.log(binary); // 输出: "1" 这里的 1 是二进制的1 也就是 0b1 

3、位运算的方法

按位操作符在 JS 中会操作当作32位比特序列进行操作, 可以理解为内部会将操作数转换成二进制形式, 并对每一位进行操作

下面是常见的按位操作符

    1. 按位与 (&) 两个操作数进行AND操作, 两个操作数对应位置上都是1,结果为1,否则为0
    1. 按位或(|) 两个操作数的每一位进行异或操作, 对应位置至少有一个1,结果就是1,否则为0
    1. 按位异或(^) 两个操作数对应位置上值相等,结果为0,否则为1 (简化记忆方式: 当两个操作数相应的比特位有且只有一个 1 时,结果为 1,否则为 0 )
    1. 按位非 (~)对操作数的每一位进行取反, 0 变 1 、 1 变 0 转成有符号整数

一看很蒙, 分别举例说明 按位或(|)

let a = 5 
let b =3 
let resultAnd = 5 | 3  // 输出 7

解释: 
先分别转化为32位的二进制, 5 => (5).toString(2) => '101'
同理 3 =>(3).toString(2) => '11'
计算过程如下: 位数不够补0 

101
011
111

按位或是有一位为1就为1, 所以计算结果为二进制 111
转成十进制 (0b111).toString(10) , 结果为7 

按位与(&)

let a = 5 
let b =3 
let resultAnd = 5 & 3  // 输出 1
仍然转化成二进制, 对齐, 位数不够补0 ,计算格式如下, 按位与是两个都为1才为1,否则为0 

101
011
001 

输出结果为001 转为十进制 (0b001).toString() => 1 

按位异或( ^ )

在来一个按位异或
let a = 5 
let b =3 
let resultAnd = 5 ^ 3  // 输出 6

通过是上述方式计算,位数不够补0 , 方便记忆的规则是仅有一位1时,结果为1,否则为0 

101
011
110

按这个规则,我们输出的是二进制 010 转成十进制是 (0b110).toString() => 6

按位非(~)

let a = 5 
~a => -6 
这里就很费劲, 为什么会这样呢? 
按我么之前的 a.toString(2) 为二进制 101 
然后按规则转化取反

101
=> 
010 

结果应该是-010啊, 转成二进制,应该是-8, 可结果为什么是 -6呢 ,-6 的二进制 是 -110, 看起来不太对劲, 其实是忽略了一条, 操作数转换为的是32位的二进制表示, 不够的位数通过0补码, 我们之前的操作符因为两两对齐, 通常不需要补齐到32位, 但是按位非,对自身的取反操作就需要考虑全体值了, 正确的转化应该是这样的

(5).toString(2).padLeft(32,'0')

取反
00000000000000000000000000000101
11111111111111111111111111111010

然后把11111111111111111111111111111010转化成有符号整数, 有符号整数是个重难点, 可以详细看转码, 结果 为 -6 

三、权限设置

上面的基础知识就完全可以让我们设计一个会员权限了, 会员权限要有一定的约束,比如权限码唯一, 增加删除权限简单, 校验简单

接下来我们简单模拟读写修改删除四个权限, 通过操作符解决上面的问题

const READ = 0b1; // 0001 => 1
const CREATE = 0b10; // 00010 => 2
const UPDATE = 0b100; // 000100 => 4
const DELETE = 0b1000; // 0001000 => 8

我们通过四个变量, READ、CREATE、UPDATE、DELETE 分别代表了读、 写、 修改、 删除, 并且附了一个二进制的值

1、 增加权限

给用户赋不同权限可以用按位或 比如给用户设置 读、写、更新权限

const user = READ | CREATE | UPDATE // => 7

2、验证权限

验证权限 我们通过 按位与 来判断

 const user = READ | CREATE | UPDATE // => 7
 方式一 通过0判断, 存在权限结果一定不为0 
 console.log(user & READ) // 1 
 console.log(user & DELETE) // 0
 
 方式二 
 console.log((user & READ) === READ) // true
 console.log((user & DELETE) === DELETE)  // false

3、删除权限

删除权限有两种方式 按位异或 或者 按位非

1、 使用按位异 或 按位非

方法一

 const user = READ | CREATE | UPDATE 
 const user2 = user ^ READ // 6 
 (user2 & READ) === READ //=> false 

按位异 可以理解为 toggle 当内部有的时候,可以删除, 没有权限的时候是增加权限, 如在上面基础上我们再次 user ^ READ (user2 & READ) === READ 就为 true 了, 如果只想让其删除,可以配合上面的查询,有在执行,或者用下面的方法

方法二: 按位与 与要删除的权限取反

 const user = READ | CREATE | UPDATE 
 const user2 = user & (~CREATE) // 5 
 (user2 & READ) === READ //=> true 
 (user2 & CREATE) === CREATE //=> false

至此, 对用户的权限的增删改查,其实已经全部完成了, 可以封装成方法, 通过不同的用户角色进行授权, 校验

三、总结

通过位运算的操作, 我们可以很容易的,对用户权限进行操作和处理, 很多后端都是通过这样处理的, 二进制的处理具有存储空间小, 传输数据量小的特点, 对与权限类的操作非常合适。