用户权限还能这么玩,颠覆你的认知(二)

1,649 阅读8分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

欢迎来到掘金,这里是小小切图仔_可笑可笑😁😁。

我们接着上一篇的内容继续往下讲,如果想看来龙去脉的可以去回顾上一篇“用户权限还能这么玩,颠覆你的认知(一)

还记得我们上一篇说到的三个议题吗?

  1. javaScript的进制位运算
  2. 权限的判断
  3. 用后感

一· 进制的位运算

1.2. 位运算

JavaScript 中位运算操作符有

运算符用法描述
按位与(AND)a & b对于每一个比特位,只有两个操作数相应的比特位都是 1 时,结果才为 1,否则为 0。
按位或(OR)a | b对于每一个比特位,当两个操作数相应的比特位至少有一个 1 时,结果为 1,否则为 0。
按位异或(XOR)a ^ b对于每一个比特位,当两个操作数相应的比特位有且只有一个 1 时,结果为 1,否则为 0。
按位非(NOT)~a反转操作数的比特位,即 0 变成 1,1 变成 0。
左移(Left shift)a << b将 a 的二进制形式向左移 b (< 32) 比特位,右边用 0 填充。
有符号右移a >> b将 a 的二进制表示向右移 b (< 32) 位,丢弃被移出的位。
无符号右移a >>> b将 a 的二进制表示向右移 b (< 32) 位,丢弃被移出的位,并使用 0 在左侧填充。

上一篇我们只讲到了按位异或(^),接下来我们会把剩下的讲完

1.2.5 有符号左移(<<)

有符号左移会将32位二进制数的所有位向左移动指定位数。如:

var num = 2; // 二进制10 
num = num << 5; // 二进制1000000,十进制64

如果要求2的n次方,可以这样:

function power(n) {
    return 1 << n;
}

power(5); // 32

1的二进制是01,左移5位就是0100000,十进制就是2的5次方32。

1.2.6 有符号右移(>>)

有符号右移会将32位二进制数的所有位向右移动指定位数。如:

var num = 64; // 二进制1000000
num = num >> 5; // 二进制10,十进制2

求一个数的二分之一:

var num = 64 >> 1; // 32

有符号左移与右移不会影响符号位。

1.2.7 无符号右移(>>>)

正数的无符号右移与有符号右移结果是一样的。负数的无符号右移会把符号位也一起移动,而且无符号右移会把负数的二进制码当成正数的二进制码:

var num = -64; // 11111111111111111111111111000000
num = num >>> 5; // 134217726

所以,我们可以利用无符号右移来判断一个数的正负:

function isPos(n) {
return (n === (n >>> 0)) ? true : false; 	
}

isPos(-1); // false
isPos(1); // true

-1>>>0虽然没有向右移动位数,但-1的二进制码已经变成了正数的二进制码:

11111111111111111111111111111111 

所以-1>>>0的值为4294967295。

上面就是我们 JavaScript 的进制位运算了,感兴趣的小伙伴可以继续深入脑补这些知识,我们进制的位运算就先讲到这里,下面我们就会讲到我们第二个议题

二. 权限的判断

我们传统的权限系统里,对权限判断都是用关联关系去维护的,后台要把数据存在很多在表中,存很多相关的字段,比如用户和权限的关联,用户和角色的关联。系统越大,关联关系越多,越难以维护。而引入位运算,可以巧妙的解决该问题。

在讲“位运算在权限系统中的使用”之前,我们先假定两个前提,下文所有的讨论都是基于这两个前提的:

每种权限码都是唯一的(这是显然的)

所有权限码的二进制数形式,有且只有一位值为 1,其余全部为 0(2^n)

如果用户权限和权限码,全部使用二级制数字表示,再结合上面位运算的例子,分析位运算的特点,我们可以发现:

| 可以用来赋予权限

& 可以用来校验权限

为了讲的更明白,这里用 Linux 中的实例分析下,Linux 的文件权限分为读、写和执行,有字母和数字等多种表现形式:

权限字母表示数字表示二进制
r40b100
w20b010
执行x10b001

可以看到,权限用 1、2、4(也就是 2^n)表示,转换为二进制后,都是只有一位是 1,其余为 0。我们通过几个例子看下,如何利用二进制的特点执行权限的添加,校验和删除。

2.1 添加权限

先与后台确定好权限列表,也就是每一种权限对应一个唯一的权限码

我们是使用了16进制定义,不管是16进制还是二进制都可以,包括菜单、按钮、数据呀,如下:

权限英文表示权限的16进制码权限的中文名
HighRisk0x00001高级风控管理
MiddleRisk0x00002中级风控管理
LowRisk0x00004低级风控管理

定义权限列表,就是新增权限了


let permission = {
    HighRisk:  0x00001,
    MiddleRisk:  0x00002,
    LowRisk:  0x00004
}
 
 
// 给用户赋全部权限(使用前面讲的 | 操作)
 
let user = permission.HighRisk | permission.HighRisk | permission.LowRisk
 
 
console.log(user)
 
// 5
 
 
console.log(user.toString(2))
 
// 101

利用 | 位运算之后,可以直接把预算完之后的结果保存到后台,或者在转一次二进制给后台,这都是你自己跟后台的约定

2.2 校验权限

添加完权限之后,我们就来校验权限:

let permission = {
    HighRisk:  0x00001,
    MiddleRisk:  0x00002,
    LowRisk:  0x00004
}


// 给用户赋 HighRisk MiddleRisk 两个权限
 
let user = permission.HighRisk | permission.MiddleRisk
 
// user = 3
 
// user = 0x00003 (十六进制)
 
 
console.log((user & permission.HighRisk) === permission.HighRisk) // true 有 HighRisk 权限
 
console.log((user & permission.MiddleRisk) === permission.MiddleRisk) // true 有 MiddleRisk 权限
 
console.log((user & permission.LowRisk) === permission.LowRisk) // false 没有 LowRisk 权限

如前所料,通过 用户权限 & 权限 code === 权限 code 就可以判断出用户是否拥有该权限。

2.3 删除权限

我们讲了用 | 赋予权限,使用 & 判断权限,那么删除权限呢?删除权限的本质其实是将指定位置上的 1 重置为 0。上个例子里用户权限是 0x00003,拥有HighRisk和MiddleRisk两个权限,现在想删除MiddleRisk的权限,本质上就是变成 0x00002即可。

那么具体怎么操作呢?其实有两种方案,最简单的就是异或 ^,按照上文的介绍“当两个操作数相应的比特位有且只有一个 1 时,结果为 1,否则为 0”,所以异或其实是 toggle 操作,无则增,有则减:

let permission = {
    HighRisk:  0x00001,
    MiddleRisk:  0x00002,
    LowRisk:  0x00004
}
let user = 0x00003 1.  // 有 HighRisk MiddleRisk 两个权限

// 执行异或操作,删除 MiddleRisk 权限
 
user = user ^ permission.MiddleRisk
 
 
console.log((user & permission.HighRisk) === permission.HighRisk) // true 有 HighRisk 权限
 
console.log((user & permission.MiddleRisk) === permission.MiddleRisk) // false 没有 MiddleRisk 权限
 
console.log((user & permission.LowRisk) === permission.LowRisk) // false 没有 LowRisk 权限
 
 
console.log(user.toString(16)) // 现在 user 是 0x00002
 
 
// 再执行一次异或操作
 
user = user ^ permission.MiddleRisk
 
 
console.log((user & permission.HighRisk) === permission.HighRisk) // true 有 HighRisk 权限
 
console.log((user & permission.MiddleRisk) === permission.MiddleRisk) // true 有 MiddleRisk 权限
 
console.log((user & permission.LowRisk) === permission.LowRisk) // false 没有 LowRisk 权限

console.log(user.toString(16)) // 现在 user 又变回 0x00003

那么如果单纯的想删除权限(而不是无则增,有则减)怎么办呢?答案是执行 &(~code),先取反,再执行与操作:

let permission = {
    HighRisk:  0x00001,
    MiddleRisk:  0x00002,
    LowRisk:  0x00004
}
let user = 0x00003 1.  // 有 HighRisk MiddleRisk 两个权限

// 执行异或操作,删除 MiddleRisk 权限
 
user = user & (~permission.MiddleRisk)
 
 
console.log((user & permission.HighRisk) === permission.HighRisk) // true 有 HighRisk 权限
 
console.log((user & permission.MiddleRisk) === permission.MiddleRisk) // false 没有 MiddleRisk 权限
 
console.log((user & permission.LowRisk) === permission.LowRisk) // false 没有 LowRisk 权限
 
 
console.log(user.toString(16)) // 现在 user 是 0x00002
 
 
// 再执行一次异或操作
 
user = user & (~permission.MiddleRisk)
 
 
console.log((user & permission.HighRisk) === permission.HighRisk) // true 有 HighRisk 权限
 
console.log((user & permission.MiddleRisk) === permission.MiddleRisk) // true 有 MiddleRisk 权限
 
console.log((user & permission.LowRisk) === permission.LowRisk) // false 没有 LowRisk 权限

console.log(user.toString(16)) // 现在 user 还是 0x00002

以上就是我使用进制位运算来做权限判断的实际逻辑,大家可以试一下!!

三· 用后感

如果按照当前使用最广泛的 RBAC 模型设计权限系统,那么一般会有这么几个实体:应用,权限,角色,用户。用户权限可以直接来自权限,也可以来自角色:

一个应用下有多个权限

权限和角色是多对多的关系

用户和角色是多对多的关系

用户和权限是多对多的关系

在此种模型下,一般会有用户与权限,用户与角色,角色与权限的对应关系表。想象一个商城后台权限管理系统,可能会有上万,甚至十几万店铺(应用),每个店铺可能会有数十个用户,角色,权限。随着业务的不断发展,刚才提到的那三张对应关系表会越来越大,越来越难以维护。

而进制转换的方法则可以省略对应关系表,减少查询,节省空间。当然,省略掉对应关系不是没有坏处的,例如下面几个问题:

如何高效的查找我的权限?

如何高效的查找拥有某权限的所有用户?

如何控制权限的有效期?

所以进制转换的方案比较适合刚才提到的应用极其多,而每个应用中用户,权限,角色数量较少的场景。

建议大家可以尝试用这种方法,毕竟能提高更好的效率,何乐而不为呢。

创作不易,请多多支持,点点赞、点点关注与收藏,就是对我最大的支持,感谢观看我的文章,这里是小小切图仔_可笑可笑