在JavaScript/TypeScript中用二进制掩码进行权限验证
前置知识
两个简单的、关于二进制数的前置知识——
二进制数的字面量
要在TS/JS中使用字面量书写二进制数字,只需在数字前加一个0b即可:
const a = 0b001; // 十进制的1
const b = 0b010; // 十进制的2
const c = 0b100; // 十进制的4
位运算
我不打算在这里详细讲位运算,只列举几个本文会涉及的位运算的例子:
// 按位或
0b001 | 0b100 === 0b101
0b110 | 0b101 === 0b111
// 按位与
0b001 & 0b110 === 0b000
0b001 & 0b111 === 0b001
// 按位非
~0b010 === 0b101
~0b110 === 0b001
// 移位运算
0b1 << 2 === 0b100
0b100 >> 1 === 0b10
用二进制掩码验证权限
利用二进制运算,可以进行权限验证。
假设我们的账户有三种权限,分别是读、写、执行,分别用三个二进制掩码进行分配。
const PERMISSION_R = 0b001; // 读
const PERMISSION_W = 0b010; // 写
const PERMISSION_X = 0b100; // 执行
因为有三种权限,所以需要用3位(3 bit)的二进制数,1代表有权限,0代表无权限。
然后,将以上三种权限按需求通过按位或进行组合:
const ADMIN = PERMISSION_R | PERMISSION_W | PERMISSION_X
const NORMAL = PERMISSION_R | PERMISSION_W
const GUEST = PERMISSION_R
ADMIN类型的账户拥有全部三种权限,NORMAL类型的账户可以读写,但不能执行,GUEST类型的账户则只读,不允许写入和执行。
如果要验证某个账户是否拥有某种权限,可以这么做:
const PERMISSION_R = 0b001; // 读
const PERMISSION_W = 0b010; // 写
const PERMISSION_X = 0b100; // 执行
const ADMIN = PERMISSION_R | PERMISSION_W | PERMISSION_X
const NORMAL = PERMISSION_R | PERMISSION_W
const GUEST = PERMISSION_R
// 验证
function verify(user: number, bitmark: number) {
return (user & bitmark) === bitmark
}
const John = 0b011; // 假设这是John这个账户权限的二进制码
console.log(verify(John, PERMISSION_W)) // 验证John是否有写入的权限
console.log(verify(John, PERMISSION_R | PERMISSION_W)) // 验证John是否同时拥有读取和写入的权限
console.log(verify(John, ADMIN)) // 验证John是否为ADMIN类型的账户
用移位运算动态创建二进制掩码
上面的例子中,我们有三种权限,所以用到了三位数的二进制数,但假如以后有了更多的权限类型,我们就得手动增加二进制的位数,显然很不方便。
因此,我们可以利用移位运算动态创建二进制掩码:
class Bitmark {
static counter = 0 // 移动位数
static gen() {
const bitmark = 0b1 << Bitmark.counter
Bitmark.counter++
return bitmark
}
}
// 生成权限
const PERMISSION_R = Bitmark.gen() // 0b1
const PERMISSION_W = Bitmark.gen() // 0b10
const PERMISSION_X = Bitmark.gen() // 0b100
每调用一次gen()方法,就能生成一个更高位数的二进制数,如果有更多的权限类型,只需继续调用即可。
如果不喜欢这种写法,我们可以将其改成函数:
const genBitmark = function() {
let counter = 0
return function() {
return 0b1 << counter++
}
}()
// 生成权限
const PERMISSION_R = genBitmark() // 0b1
const PERMISSION_W = genBitmark() // 0b10
const PERMISSION_X = genBitmark() // 0b100
但为了避免创建全局变量,这里我使用了立即执行函数,将counter放在了闭包环境中。
写一个完整的例子
最后写一个较为完整的例子
// 生成二进制掩码
const genBitmark = function() {
let counter = 0
return function() {
return 0b1 << counter++
}
}()
// 三种用户类型,用于User构造器的参数
type UserType = 'admin' | 'guest' | 'normal'
// User构造器
class User {
bitmark: number
// 生成权限类型
static R = genBitmark() // 读取权限
static W = genBitmark() // 写入权限
static X = genBitmark() // 执行权限
// 创建用户类型,赋值对应的权限
static ADMIN = User.R | User.W | User.X
static NORMAL = User.R | User.W
static GUEST = User.R
constructor(public type: UserType) {
switch (type) {
case 'admin':
this.bitmark = User.ADMIN
break
case 'normal':
this.bitmark = User.NORMAL
break
case 'guest':
this.bitmark = User.GUEST
break
}
}
// 权限校验
verify(bitmark: number) {
return (this.bitmark & bitmark) === bitmark
}
// 权限添加
add(bitmark: number) {
this.bitmark |= bitmark
}
// 权限删除
remove(bitmark: number) {
this.bitmark &= ~bitmark
}
// 读取的权限
get readable() {
return this.verify(User.R)
}
// 写入的权限
get writable() {
return this.verify(User.W)
}
// 执行的权限
get executable() {
return this.verify(User.X)
}
}
测试
import assert from 'node:assert'
const user = new User('normal')
const admin = new User('admin')
const guest = new User('guest')
assert.equal(user.readable, true)
assert.equal(user.executable, false)
assert.equal(user.verify(User.R), true)
assert.equal(user.verify(User.W), true)
assert.equal(user.verify(User.X), false)
assert.equal(user.verify(User.R | User.W), true)
assert.equal(user.verify(User.R | User.W | User.X), false)
assert.equal(admin.verify(User.R), true)
assert.equal(admin.verify(User.W), true)
assert.equal(admin.verify(User.X), true)
assert.equal(guest.verify(User.R), true)
assert.equal(guest.verify(User.W), false)
assert.equal(guest.verify(User.X), false)
guest.add(User.W)
assert.equal(guest.verify(User.R), true)
assert.equal(guest.verify(User.W), true)
assert.equal(guest.verify(User.X), false)
admin.remove(User.X)
assert.equal(admin.verify(User.R), true)
assert.equal(admin.verify(User.W), true)
assert.equal(admin.verify(User.X), false)