1. 內(nèi)容概要
本文主要討論以下兩個(gè)問(wèn)題:
- JAVAScript 的位運(yùn)算:先簡(jiǎn)單回顧下位運(yùn)算,平時(shí)用的少,相信不少人和我一樣忘的差不多了
- 權(quán)限設(shè)計(jì):根據(jù)位運(yùn)算的特點(diǎn),設(shè)計(jì)一個(gè)權(quán)限系統(tǒng)(添加、刪除、判斷等)
2. JavaScript 位運(yùn)算
2.1. Number
在講位運(yùn)算之前,首先簡(jiǎn)單看下 JavaScript 中的 Number,下文需要用到。
在 JavaScript 里,數(shù)字均為基于 IEEE 754 標(biāo)準(zhǔn)的雙精度 64 位的浮點(diǎn)數(shù),引用維基百科的圖片,它的結(jié)構(gòu)長(zhǎng)這樣:
- sign bit(符號(hào)): 用來(lái)表示正負(fù)號(hào)
- exponent(指數(shù)): 用來(lái)表示次方數(shù)
- mantissa(尾數(shù)): 用來(lái)表示精確度
也就是說(shuō)一個(gè)數(shù)字的范圍只能在 -(2^53 -1) 至 2^53 -1 之間。
既然講到這里,就多說(shuō)一句:0.1 + 0.2 算不準(zhǔn)的原因也在于此。浮點(diǎn)數(shù)用二進(jìn)制表達(dá)時(shí)是無(wú)窮的,且最多 53 位,必須截?cái)啵M(jìn)而產(chǎn)生誤差。最簡(jiǎn)單的解決辦法就是放大一定倍數(shù)變成整數(shù),計(jì)算完成后再縮小。不過(guò)更穩(wěn)妥的辦法是使用下文將會(huì)提到的 math.js 等工具庫(kù)。
此外還有四種數(shù)字進(jìn)制:
// 十進(jìn)制 123456789 0 // 二進(jìn)制:前綴 0b,0B 0b10000000000000000000000000000000 // 2147483648 0b01111111100000000000000000000000 // 2139095040 0B00000000011111111111111111111111 // 8388607 // 八進(jìn)制:前綴 0o,0O(以前支持前綴 0) 0o755 // 493 0o644 // 420 // 十六進(jìn)制:前綴 0x,0X 0xFFFFFFFFFFFFFFFFF // 295147905179352830000 0x123456789ABCDEF // 81985529216486900 0XA // 10
好了,Number 就說(shuō)這么多,接下來(lái)看 JavaScript 中的位運(yùn)算。
2.2. 位運(yùn)算
按位操作符將其操作數(shù)當(dāng)作 32 位的比特序列(由 0 和 1 組成)操作,返回值依然是標(biāo)準(zhǔn)的 JavaScript 數(shù)值。JavaScript 中的按位操作符有:
下面舉幾個(gè)例子,主要看下 AND 和 OR:
# 例子1 A = 10001001 B = 10010000 A | B = 10011001 # 例子2 A = 10001001 C = 10001000 A | C = 10001001
# 例子1 A = 10001001 B = 10010000 A & B = 10000000 # 例子2 A = 10001001 C = 10001000 A & C = 10001000
3. 位運(yùn)算在權(quán)限系統(tǒng)中的使用
傳統(tǒng)的權(quán)限系統(tǒng)里,存在很多關(guān)聯(lián)關(guān)系,如用戶和權(quán)限的關(guān)聯(lián),用戶和角色的關(guān)聯(lián)。系統(tǒng)越大,關(guān)聯(lián)關(guān)系越多,越難以維護(hù)。而引入位運(yùn)算,可以巧妙的解決該問(wèn)題。
在講“位運(yùn)算在權(quán)限系統(tǒng)中的使用”之前,我們先假定兩個(gè)前提,下文所有的討論都是基于這兩個(gè)前提的:
- 每種權(quán)限碼都是唯一的(這是顯然的)
- 所有權(quán)限碼的二進(jìn)制數(shù)形式,有且只有一位值為 1,其余全部為 0(2^n)
如果用戶權(quán)限和權(quán)限碼,全部使用二級(jí)制數(shù)字表示,再結(jié)合上面 AND 和 OR 的例子,分析位運(yùn)算的特點(diǎn),不難發(fā)現(xiàn):
- | 可以用來(lái)賦予權(quán)限
- & 可以用來(lái)校驗(yàn)權(quán)限
為了講的更明白,這里用 linux 中的實(shí)例分析下,Linux 的文件權(quán)限分為讀、寫(xiě)和執(zhí)行,有字母和數(shù)字等多種表現(xiàn)形式:
權(quán)限 字母表示 數(shù)字表示 二進(jìn)制 讀 r 4 0b100 寫(xiě) w 2 0b010 執(zhí)行 x 1 0b001
可以看到,權(quán)限用 1、2、4(也就是 2^n)表示,轉(zhuǎn)換為二進(jìn)制后,都是只有一位是 1,其余為 0。我們通過(guò)幾個(gè)例子看下,如何利用二進(jìn)制的特點(diǎn)執(zhí)行權(quán)限的添加,校驗(yàn)和刪除。
3.1. 添加權(quán)限
let r = 0b100 let w = 0b010 let x = 0b001 // 給用戶賦全部權(quán)限(使用前面講的 | 操作) let user = r | w | x console.log(user) // 7 console.log(user.toString(2)) // 111 // r = 0b100 // w = 0b010 // r = 0b001 // r|w|x = 0b111
可以看到,執(zhí)行 r | w | x 后,user 的三位都是 1,表明擁有了全部三個(gè)權(quán)限。
Linux 下出現(xiàn)權(quán)限問(wèn)題時(shí),最粗暴的解決方案就是 chmod 777 xxx,這里的 7 就代表了:可讀,可寫(xiě),可執(zhí)行。而三個(gè) 7 分別代表:文件所有者,文件所有者所在組,所有其他用戶。
3.2. 校驗(yàn)權(quán)限
剛才演示了權(quán)限的添加,下面演示權(quán)限校驗(yàn):
let r = 0b100 let w = 0b010 let x = 0b001 // 給用戶賦 r w 兩個(gè)權(quán)限 let user = r | w // user = 6 // user = 0b110 (二進(jìn)制) console.log((user & r) === r) // true 有 r 權(quán)限 console.log((user & w) === w) // true 有 w 權(quán)限 console.log((user & x) === x) // false 沒(méi)有 x 權(quán)限
如前所料,通過(guò) 用戶權(quán)限 & 權(quán)限 code === 權(quán)限 code 就可以判斷出用戶是否擁有該權(quán)限。
3.3. 刪除權(quán)限
我們講了用 | 賦予權(quán)限,使用 & 判斷權(quán)限,那么刪除權(quán)限呢?刪除權(quán)限的本質(zhì)其實(shí)是將指定位置上的 1 重置為 0。上個(gè)例子里用戶權(quán)限是 0b110,擁有讀和寫(xiě)兩個(gè)權(quán)限,現(xiàn)在想刪除讀的權(quán)限,本質(zhì)上就是將第三位的 1 重置為 0,變?yōu)?0b010:
let r = 0b100 let w = 0b010 let x = 0b001 let user = 0b010; console.log((user & r) === r) // false 沒(méi)有 r 權(quán)限 console.log((user & w) === w) // true 有 w 權(quán)限 console.log((user & x) === x) // false 沒(méi)有 x 權(quán)限
那么具體怎么操作呢?其實(shí)有兩種方案,最簡(jiǎn)單的就是異或 ^,按照上文的介紹“當(dāng)兩個(gè)操作數(shù)相應(yīng)的比特位有且只有一個(gè) 1 時(shí),結(jié)果為 1,否則為 0”,所以異或其實(shí)是 toggle 操作,無(wú)則增,有則減:
let r = 0b100 let w = 0b010 let x = 0b001 let user = 0b110 // 有 r w 兩個(gè)權(quán)限 // 執(zhí)行異或操作,刪除 r 權(quán)限 user = user ^ r console.log((user & r) === r) // false 沒(méi)有 r 權(quán)限 console.log((user & w) === w) // true 有 w 權(quán)限 console.log((user & x) === x) // false 沒(méi)有 x 權(quán)限 console.log(user.toString(2)) // 現(xiàn)在 user 是 0b010 // 再執(zhí)行一次異或操作 user = user ^ r console.log((user & r) === r) // true 有 r 權(quán)限 console.log((user & w) === w) // true 有 w 權(quán)限 console.log((user & x) === x) // false 沒(méi)有 x 權(quán)限 console.log(user.toString(2)) // 現(xiàn)在 user 又變回 0b110
那么如果單純的想刪除權(quán)限(而不是無(wú)則增,有則減)怎么辦呢?答案是執(zhí)行 &(~code),先取反,再執(zhí)行與操作:
let r = 0b100 let w = 0b010 let x = 0b001 let user = 0b110 // 有 r w 兩個(gè)權(quán)限 // 刪除 r 權(quán)限 user = user & (~r) console.log((user & r) === r) // false 沒(méi)有 r 權(quán)限 console.log((user & w) === w) // true 有 w 權(quán)限 console.log((user & x) === x) // false 沒(méi)有 x 權(quán)限 console.log(user.toString(2)) // 現(xiàn)在 user 是 0b010 // 再執(zhí)行一次 user = user & (~r) console.log((user & r) === r) // false 沒(méi)有 r 權(quán)限 console.log((user & w) === w) // true 有 w 權(quán)限 console.log((user & x) === x) // false 沒(méi)有 x 權(quán)限 console.log(user.toString(2)) // 現(xiàn)在 user 還是 0b010,并不會(huì)新增
4. 局限性和解決辦法
前面我們回顧了 JavaScript 中的 Number 和位運(yùn)算,并且了解了基于位運(yùn)算的權(quán)限系統(tǒng)原理和 Linux 文件系統(tǒng)權(quán)限的實(shí)例。
上述的所有都有前提條件:1、每種權(quán)限碼都是唯一的;2、每個(gè)權(quán)限碼的二進(jìn)制數(shù)形式,有且只有一位值為 1(2^n)。也就是說(shuō),權(quán)限碼只能是 1, 2, 4, 8,...,1024,...而上文提到,一個(gè)數(shù)字的范圍只能在 -(2^53 -1) 和 2^53 -1 之間,JavaScript 的按位操作符又是將其操作數(shù)當(dāng)作 32 位比特序列的。那么同一個(gè)應(yīng)用下可用的權(quán)限數(shù)就非常有限了。這也是該方案的局限性。
為了突破這個(gè)限制,這里提出一個(gè)叫“權(quán)限空間”的概念,既然權(quán)限數(shù)有限,那么不妨就多開(kāi)辟幾個(gè)空間來(lái)存放。
基于權(quán)限空間,我們定義兩個(gè)格式:
- 權(quán)限 code,字符串,形如 index,pos。其中 pos 表示 32 位二進(jìn)制數(shù)中 1 的位置(其余全是 0); index 表示權(quán)限空間,用于突破 JavaScript 數(shù)字位數(shù)的限制,是從 0 開(kāi)始的正整數(shù),每個(gè)權(quán)限code都要?dú)w屬于一個(gè)權(quán)限空間。index 和 pos 使用英文逗號(hào)隔開(kāi)。
- 用戶權(quán)限,字符串,形如 1,16,16。英文逗號(hào)分隔每一個(gè)權(quán)限空間的權(quán)限值。例如 1,16,16 的意思就是,權(quán)限空間 0 的權(quán)限值是 1,權(quán)限空間 1 的權(quán)限值是 16,權(quán)限空間 2 的權(quán)限是 16。
干說(shuō)可能不好懂,直接上代碼:
// 用戶的權(quán)限 code
let userCode = ""
// 假設(shè)系統(tǒng)里有這些權(quán)限
// 純模擬,正常情況下是按順序的,如 0,0 0,1 0,2 ...,盡可能占滿一個(gè)權(quán)限空間,再使用下一個(gè)
const permissions = {
SYS_SETTING: {
value: "0,0", // index = 0, pos = 0
info: "系統(tǒng)權(quán)限"
},
DATA_ADMIN: {
value: "0,8",
info: "數(shù)據(jù)庫(kù)權(quán)限"
},
USER_ADD: {
value: "0,22",
info: "用戶新增權(quán)限"
},
USER_EDIT: {
value: "0,30",
info: "用戶編輯權(quán)限"
},
USER_VIEW: {
value: "1,2", // index = 1, pos = 2
info: "用戶查看權(quán)限"
},
USER_DELETE: {
value: "1,17",
info: "用戶刪除權(quán)限"
},
POST_ADD: {
value: "1,28",
info: "文章新增權(quán)限"
},
POST_EDIT: {
value: "2,4",
info: "文章編輯權(quán)限"
},
POST_VIEW: {
value: "2,19",
info: "文章查看權(quán)限"
},
POST_DELETE: {
value: "2,26",
info: "文章刪除權(quán)限"
}
}
// 添加權(quán)限
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(",")
}
// 刪除權(quán)限
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(",")
}
// 判斷是否有權(quán)限
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
}
// 列出用戶擁有的全部權(quán)限
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(`權(quán)限列表: ${listPermission(userCode).join("; ")}`)
console.log("")
}
userCode = addPermission(userCode, permissions.SYS_SETTING)
log()
// userCode: "1"
// 權(quán)限列表: 系統(tǒng)權(quán)限
userCode = addPermission(userCode, permissions.POST_EDIT)
log()
// userCode: "1,,16"
// 權(quán)限列表: 系統(tǒng)權(quán)限; 文章編輯權(quán)限
userCode = addPermission(userCode, permissions.USER_EDIT)
log()
// userCode: "1073741825,,16"
// 權(quán)限列表: 系統(tǒng)權(quán)限; 用戶編輯權(quán)限; 文章編輯權(quán)限
userCode = addPermission(userCode, permissions.USER_DELETE)
log()
// userCode: "1073741825,131072,16"
// 權(quán)限列表: 系統(tǒng)權(quán)限; 用戶編輯權(quán)限; 用戶刪除權(quán)限; 文章編輯權(quán)限
userCode = delPermission(userCode, permissions.USER_EDIT)
log()
// userCode: "1,131072,16"
// 權(quán)限列表: 系統(tǒng)權(quán)限; 用戶刪除權(quán)限; 文章編輯權(quán)限
userCode = delPermission(userCode, permissions.USER_EDIT)
log()
// userCode: "1,131072,16"
// 權(quán)限列表: 系統(tǒng)權(quán)限; 用戶刪除權(quán)限; 文章編輯權(quán)限
userCode = delPermission(userCode, permissions.USER_DELETE)
userCode = delPermission(userCode, permissions.SYS_SETTING)
userCode = delPermission(userCode, permissions.POST_EDIT)
log()
// userCode: "0,0,0"
// 權(quán)限列表:
userCode = addPermission(userCode, permissions.SYS_SETTING)
log()
// userCode: "1,0,0"
// 權(quán)限列表: 系統(tǒng)權(quán)限
除了通過(guò)引入權(quán)限空間的概念突破二進(jìn)制運(yùn)算的位數(shù)限制,還可以使用 math.js 的 bignumber,直接運(yùn)算超過(guò) 32 位的二進(jìn)制數(shù),具體可以看它的文檔,這里就不細(xì)說(shuō)了。
5. 適用場(chǎng)景和問(wèn)題
如果按照當(dāng)前使用最廣泛的 RBAC 模型設(shè)計(jì)權(quán)限系統(tǒng),那么一般會(huì)有這么幾個(gè)實(shí)體:應(yīng)用,權(quán)限,角色,用戶。用戶權(quán)限可以直接來(lái)自權(quán)限,也可以來(lái)自角色:
- 一個(gè)應(yīng)用下有多個(gè)權(quán)限
- 權(quán)限和角色是多對(duì)多的關(guān)系
- 用戶和角色是多對(duì)多的關(guān)系
- 用戶和權(quán)限是多對(duì)多的關(guān)系
在此種模型下,一般會(huì)有用戶與權(quán)限,用戶與角色,角色與權(quán)限的對(duì)應(yīng)關(guān)系表。想象一個(gè)商城后臺(tái)權(quán)限管理系統(tǒng),可能會(huì)有上萬(wàn),甚至十幾萬(wàn)店鋪(應(yīng)用),每個(gè)店鋪可能會(huì)有數(shù)十個(gè)用戶,角色,權(quán)限。隨著業(yè)務(wù)的不斷發(fā)展,剛才提到的那三張對(duì)應(yīng)關(guān)系表會(huì)越來(lái)越大,越來(lái)越難以維護(hù)。
而進(jìn)制轉(zhuǎn)換的方法則可以省略對(duì)應(yīng)關(guān)系表,減少查詢,節(jié)省空間。當(dāng)然,省略掉對(duì)應(yīng)關(guān)系不是沒(méi)有壞處的,例如下面幾個(gè)問(wèn)題:
- 如何高效的查找我的權(quán)限?
- 如何高效的查找擁有某權(quán)限的所有用戶?
- 如何控制權(quán)限的有效期?
所以進(jìn)制轉(zhuǎn)換的方案比較適合剛才提到的應(yīng)用極其多,而每個(gè)應(yīng)用中用戶,權(quán)限,角色數(shù)量較少的場(chǎng)景。
6. 其他方案
除了二進(jìn)制方案,當(dāng)然還有其他方案可以達(dá)到類似的效果,例如直接使用一個(gè)1和0組成的字符串,權(quán)限點(diǎn)對(duì)應(yīng)index,1表示擁有權(quán)限,0表示沒(méi)有權(quán)限。舉個(gè)例子:添加 0、刪除 1、編輯 2,用戶A擁有添加和編輯的權(quán)限,則 userCode 為 101;用戶B擁有全部權(quán)限,userCode 為 111。這種方案比二進(jìn)制轉(zhuǎn)換簡(jiǎn)單,但是浪費(fèi)空間。
還有利用質(zhì)數(shù)的方案,權(quán)限點(diǎn)全部為質(zhì)數(shù),用戶權(quán)限為他所擁有的全部權(quán)限點(diǎn)的乘積。如:權(quán)限點(diǎn)是 2、3、5、7、11,用戶權(quán)限是 5 * 7 * 11 = 385。這種方案麻煩的地方在于獲取質(zhì)數(shù)(新增權(quán)限點(diǎn))和質(zhì)因數(shù)分解(判斷權(quán)限),權(quán)限點(diǎn)特別多的時(shí)候就快成 RSA 了,如果只有增刪改查個(gè)別幾個(gè)權(quán)限,倒是可以考慮。
來(lái)源:掘金 鏈接:https://juejin.im/post/5dc36f39e51d4529ed292910






