本文摘至: 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 了,若是隻有增刪改查個別幾個權限,卻是能夠考慮。