在JavaScript/TypeScript中用二进制掩码进行权限验证

274 阅读4分钟

在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)