本文摘至: musicfe.dev/javascript-…javascript
本文主要讨论如下两个问题:html
在讲位运算以前,首先简单看下 JavaScript 中的 Number
,下文须要用到。 在 JavaScript 里,数字均为基于 IEEE 754 标准的双精度 64 位的浮点数,引用维基百科的图片,它的结构长这样:java
也就是说一个数字的范围只能在 -(2^53 -1) 至 2^53 -1 之间。数据库
既然讲到这里,就多说一句:
0.1 + 0.2
算不许的缘由也在于此。浮点数用二进制表达时是无穷的,且最多 53 位,必须截断,进而产生偏差。最简单的解决办法就是放大必定倍数变成整数,计算完成后再缩小。不过更稳妥的办法是使用下文将会提到的 math.js 等工具库。bash
此外还有四种数字进制:ide
// 十进制
123456789
0
// 二进制:前缀 0b,0B
0b10000000000000000000000000000000 // 2147483648
0b01111111100000000000000000000000 // 2139095040
0B00000000011111111111111111111111 // 8388607
// 八进制:前缀 0o,0O(之前支持前缀 0)
0o755 // 493
0o644 // 420
// 十六进制:前缀 0x,0X
0xFFFFFFFFFFFFFFFFF // 295147905179352830000
0x123456789ABCDEF // 81985529216486900
0XA // 10
复制代码
好了,Number 就说这么多,接下来看 JavaScript 中的位运算。工具
按位操做符将其操做数看成 32 位的比特序列(由 0 和 1 组成)操做,返回值依然是标准的 JavaScript 数值。JavaScript 中的按位操做符有:ui
a & b
对于每个比特位,只有两个操做数相应的比特位都是 1 时,结果才为 1,不然为 0。spa
let a = 0b110, b = 0b100
let c = a & b
c.toString(2) // output: "100"
复制代码
a | b
对于每个比特位,当两个操做数相应的比特位至少有一个 1 时,结果为 1,不然为 0。设计
let a = 0b110, b = 0b100
let c = a | b
c.toString(2) // output: "110"
复制代码
a ^ b
对于每个比特位,当两个操做数相应的比特位有且只有一个 1 时,结果为 1,不然为 0。
let a = 0b110, b = 0b100
let c = a ^ b
c.toString(2) // output: "10"
复制代码
~a
反转操做数的比特位,即 0 变成 1,1 变成 0。
let a = 0b110
~a.toString(2) // output: -111
复制代码
a << b
将 a 的二进制形式向左移 b (< 32) 比特位,右边用 0 填充。
let a = 0b110, b = 0b100
(a << b).toString(2) // output: "1100000"
(0b1 << 0b11).toString(2) // outout: "1000"
(0b1 << 0b111).toString(2) // output: "10000000"
(0b1 << 0b1111).toString(2) // output: "1000000000000000"
复制代码
a >> b
将 a 的二进制表示向右移 b (< 32) 位,丢弃被移出的位。
let a = 0b110, b = 0b100
(a >> b).toString(2) // output: "0"
(0b1111 >> 0b11).toString(2) // output: "1"
(0b1111111 >> 0b11).toString(2) // output: "1111"
(0b10001000 >> 0b111).toString(2) // output: "1"
(0b1000100010001000 >> 0b111).toString(2) // output: "100010001"
复制代码
a >>> b
将 a 的二进制表示向右移 b (< 32) 位,丢弃被移出的位,并使用 0 在左侧填充。
let a = 0b110, b = 0b100
( a >>> b).toString(2) // output: "0"
( 0b100 >>> 0b1).toString(2) // output: "10"
( 0b100 >>> 0b10).toString(2) // output: "1"
( 0b100 >>> 0b100).toString(2) // output: "0"
( 0b100 >>> 0b1000).toString(2) // output: "0"
( 0b10001000 >>> 0b100).toString(2) // output: "1000"
( 0b10001000 >>> 0b101).toString(2) // output: "100"
( 0b10001000 >>> 0b111).toString(2) // output: "1"
复制代码
传统的权限系统里,存在不少关联关系,如:
系统越大,关联关系越多,越难以维护。而引入位运算,能够巧妙的解决该问题。
首先,咱们先假定两个前提,下文全部的讨论都是基于这两个前提的:
若是用户权限和权限码,所有使用二级制数字表示,再结合上面 AND
和 OR
的例子,分析位运算的特色,不难发现:
|
能够用来赋予权限&
能够用来校验权限为了讲的更明白,这里用 Linux 中的实例分析下,Linux 的文件权限分为读、写和执行,有字母和数字等多种表现形式:
能够看到,权限用 一、二、4(也就是 2^n)表示,转换为二进制后,都是只有一位是 1,其他为 0。咱们经过几个例子看下,如何利用二进制的特色执行权限的添加,校验和删除。
示例代码:
let r = 0b100
let w = 0b010
let x = 0b001
let user = r | w | x
user.toString(2) // output: "111"
复制代码
能够看到,执行 r | w | x 后,user 的三位都是 1,代表拥有了所有三个权限。
Linux 下出现权限问题时,最粗暴的解决方案就是 chmod 777 xxx
,这里的 7 就表明了:可读,可写,可执行。而三个 7 分别表明:文件全部者,文件全部者所在组,全部其余用户。
刚才演示了权限的添加,下面演示权限校验:
let r = 0b100
let w = 0b010
let x = 0b001
// 给用户赋 r w 两个权限
let user = r | w
// user = 6
// user = 0b110 (二进制)
console.log((user & r) === r) // true 有 r 权限
console.log((user & w) === w) // true 有 w 权限
console.log((user & x) === x) // false 没有 x 权限
复制代码
如前所料,经过 用户权限 & 权限 code === 权限 code 就能够判断出用户是否拥有该权限。
咱们讲了用 |
赋予权限,使用 &
判断权限,那么删除权限呢?
删除权限的本质实际上是将指定位置上的 1 重置为 0。 上个例子里用户权限是 0b110
,拥有读和写两个权限,如今想删除读的权限,本质上就是将第三位的 1 重置为 0,变为 0b010
:
let r = 0b100
let w = 0b010
let x = 0b001
let user = 0b010;
console.log((user & r) === r) // false 没有 r 权限
console.log((user & w) === w) // true 有 w 权限
console.log((user & x) === x) // false 没有 x 权限
复制代码
^
那么具体怎么操做呢?其实有两种方案最简单的就是异或 ^,按照上文的介绍“当两个操做数相应的比特位有且只有一个 1 时,结果为 1,不然为 0”,因此异或实际上是 toggle 操做,无则增,有则减:
let r = 0b100
let w = 0b010
let x = 0b001
let user = 0b110 // 有 r w 两个权限
// 执行异或操做,删除 r 权限
user = user ^ r
console.log((user & r) === r) // false 没有 r 权限
console.log((user & w) === w) // true 有 w 权限
console.log((user & x) === x) // false 没有 x 权限
console.log(user.toString(2)) // 如今 user 是 0b010
复制代码
缺点: 此例中若再执行一次异或操做, 会致使又拥有了r
的权限
// 再执行一次异或操做
user = user ^ r
console.log((user & r) === r) // true 有 r 权限
console.log((user & w) === w) // true 有 w 权限
console.log((user & x) === x) // false 没有 x 权限
console.log(user.toString(2)) // 如今 user 又变回 0b110
复制代码
&(~code)
(最佳方案)那么若是单纯的想删除权限(而不是无则增,有则减)怎么办呢?
答案是执行 &(~code)
,先取反,再执行与操做
let r = 0b100
let w = 0b010
let x = 0b001
let user = 0b110 // 有 r w 两个权限
// 删除 r 权限
user = user & (~r)
console.log((user & r) === r) // false 没有 r 权限
console.log((user & w) === w) // true 有 w 权限
console.log((user & x) === x) // false 没有 x 权限
console.log(user.toString(2)) // 如今 user 是 0b010
// 再执行一次
user = user & (~r)
console.log((user & r) === r) // false 没有 r 权限
console.log((user & w) === w) // true 有 w 权限
console.log((user & x) === x) // false 没有 x 权限
console.log(user.toString(2)) // 如今 user 仍是 0b010,并不会新增
复制代码
前面咱们回顾了 JavaScript 中的 Number 和位运算,而且了解了基于位运算的权限系统原理和 Linux 文件系统权限的实例。
上述的全部都有前提条件:
为了突破这个限制,这里提出一个叫 权限空间 的概念,即权限数有限,那么不妨就多开辟几个空间来存放。
基于权限空间,咱们定义两个格式:
index
,pos
。其中 :
示例代码:
// 用户的权限 code
let userCode = ""
// 假设系统里有这些权限
// 纯模拟,正常状况下是按顺序的,如 0,0 0,1 0,2 ...,尽量占满一个权限空间,再使用下一个
const permissions = {
SYS_SETTING: {
value: "0,0", // index = 0, pos = 0
info: "系统权限"
},
DATA_ADMIN: {
value: "0,8",
info: "数据库权限"
},
USER_ADD: {
value: "0,22",
info: "用户新增权限"
},
USER_EDIT: {
value: "0,30",
info: "用户编辑权限"
},
USER_VIEW: {
value: "1,2", // index = 1, pos = 2
info: "用户查看权限"
},
USER_DELETE: {
value: "1,17",
info: "用户删除权限"
},
POST_ADD: {
value: "1,28",
info: "文章新增权限"
},
POST_EDIT: {
value: "2,4",
info: "文章编辑权限"
},
POST_VIEW: {
value: "2,19",
info: "文章查看权限"
},
POST_DELETE: {
value: "2,26",
info: "文章删除权限"
}
}
// 添加权限
const addPermission = (userCode, permission) => {
const userPermission = userCode ? userCode.split(",") : []
const [index, pos] = permission.value.split(",")
userPermission[index] = (userPermission[index] || 0) | Math.pow(2, pos)
return userPermission.join(",")
}
// 删除权限
const delPermission = (userCode, permission) => {
const userPermission = userCode ? userCode.split(",") : []
const [index, pos] = permission.value.split(",")
userPermission[index] = (userPermission[index] || 0) & (~Math.pow(2, pos))
return userPermission.join(",")
}
// 判断是否有权限
const hasPermission = (userCode, permission) => {
const userPermission = userCode ? userCode.split(",") : []
const [index, pos] = permission.value.split(",")
const permissionValue = Math.pow(2, pos)
return (userPermission[index] & permissionValue) === permissionValue
}
// 列出用户拥有的所有权限
const listPermission = userCode => {
const results = []
if (!userCode) {
return results
}
Object.values(permissions).forEach(permission => {
if (hasPermission(userCode, permission)) {
results.push(permission.info)
}
})
return results
}
const log = () => {
console.log(`userCode: ${JSON.stringify(userCode, null, " ")}`)
console.log(`权限列表: ${listPermission(userCode).join("; ")}`)
console.log("")
}
userCode = addPermission(userCode, permissions.SYS_SETTING)
log()
// userCode: "1"
// 权限列表: 系统权限
userCode = addPermission(userCode, permissions.POST_EDIT)
log()
// userCode: "1,,16"
// 权限列表: 系统权限; 文章编辑权限
userCode = addPermission(userCode, permissions.USER_EDIT)
log()
// userCode: "1073741825,,16"
// 权限列表: 系统权限; 用户编辑权限; 文章编辑权限
userCode = addPermission(userCode, permissions.USER_DELETE)
log()
// userCode: "1073741825,131072,16"
// 权限列表: 系统权限; 用户编辑权限; 用户删除权限; 文章编辑权限
userCode = delPermission(userCode, permissions.USER_EDIT)
log()
// userCode: "1,131072,16"
// 权限列表: 系统权限; 用户删除权限; 文章编辑权限
userCode = delPermission(userCode, permissions.USER_EDIT)
log()
// userCode: "1,131072,16"
// 权限列表: 系统权限; 用户删除权限; 文章编辑权限
userCode = delPermission(userCode, permissions.USER_DELETE)
userCode = delPermission(userCode, permissions.SYS_SETTING)
userCode = delPermission(userCode, permissions.POST_EDIT)
log()
// userCode: "0,0,0"
// 权限列表:
userCode = addPermission(userCode, permissions.SYS_SETTING)
log()
// userCode: "1,0,0"
// 权限列表: 系统权限
复制代码
除了经过引入权限空间的概念突破二进制运算的位数限制,还可使用 math.js 的 bignumber,直接运算超过 32 位的二进制数,具体能够看它的文档,这里就不细说了。
若是按照当前使用最普遍的 RBAC 模型设计权限系统,那么通常会有这么几个实体:
用户权限能够直接来自权限,也能够来自角色:
在此种模型下,通常会有3张对应关系表:
想象一个商城后台权限管理系统,可能会有上万,甚至十几万店铺(应用),每一个店铺可能会有数十个用户,角色,权限。随着业务的不断发展,刚才提到的那三张对应关系表会愈来愈大,愈来愈难以维护。
而进制转换的方法则能够省略对应关系表,减小查询,节省空间。固然,省略掉对应关系不是没有坏处的,例以下面几个问题:
因此进制转换的方案比较适合刚才提到的应用极其多,而每一个应用中用户,权限,角色数量较少的场景。
除了二进制方案,固然还有其余方案能够达到相似的效果.
例如: 直接使用一个1和0组成的字符串,权限点对应index
举个例子:
这种方案比二进制转换简单,可是浪费空间。
还有利用质数的方案,权限点所有为质数,用户权限为他所拥有的所有权限点的乘积。如:权限点是 二、三、五、七、11,用户权限是 5 * 7 * 11 = 385。
这种方案麻烦的地方在于获取质数(新增权限点)和质因数分解(判断权限),权限点特别多的时候就快成 RSA 了,若是只有增删改查个别几个权限,却是能够考虑。