JS位运算与权限设计

1,206 阅读6分钟

一.简单回顾一下js中的位运算:

1. "&" :与运算,转化为二进制数,如果相同位数都为1则得结果为1,否则为0;

2. "|" :或运算,转化为二进制数,如果相同位数只要有一个为1则得结果为1,否则为0

3. "^" :异或运算,转化为二进制数,如果相同位数不同则得结果为1,否则为0

4. "<<" 异位运算符,1<<1,表示将1左移一位,也就是010,在二进制中代表2;

5.十进制转为别的进制:

var num = 123;

console.log(num.toString(2))

console.log(num.toString(8))//参数传想要转化的位数

别的进制转化为十进制:

var num = 110;

console.log(parseInt(num,2))//二进制转化为十进制

6.看几个简单例子

# 例子1
A = 10001001
B = 10010000
A & B = 10000000
# 例子2
A = 10001001
C = 10001000
A & C = 10001000

二.将以上操作运用于权限设计中:

假设目前系统每个租户都有一个各自的授权码,每种授权码而且是唯一的,结合上面位运算操作可进一步设计

  • | (或运算)用来赋予权限
  • & (与运算)用来校验权限
  • ^ (异或运算)用来删除权限

用户RWX权限(读/写/执行)举例:

1.添加权限

let r = 0b100 
let w = 0b010 
let x = 0b001 
// 给用户赋全部权限(使用前面讲的 | 操作) 
let user = r | w | x 

// 打印日志看下结果
console.log(user) // 7 对应二进制0b111
0b111后三位都是1,表示拥有了三个权限都拥有。

2.校验权限

// 给用户赋 r w 两个权限
let user = r | w; // user = 6 对应二进制0b110

#  通过 用户权限 & 权限 code === 权限 code 就可以判断出用户是否拥有该权限


// 打印日志看下结果

console.log((user & r) === r) // true 有 r 权限

console.log((user & w) === w) // true 有 w 权限

console.log((user & x) === x) // false 没有 x 权限。

3.删除权限

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) // true 有 r 权限

console.log((user & w) === w) // true 有 w 权限

console.log((user & x) === x) // false 没有 x 权限

console.log(user.toString(2)) // 现在 user 又变回 0b110



如果单纯的想删除权限(而不是无则增,有则减)怎么办呢?

答案是执行 &(~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,并不会新增

三.局限性和解决办法

上述的所有都有前提条件:

1、每种权限码都是唯一的;

2、每个权限码的二进制数形式,有且只有一位值为 1(2^n)

也就是说,权限码只能是 1, 2, 4, 8,...,1024,...而上文提到,一个数字的范围只能在 -(2^53 -1) 和 2^53 -1 之间,JavaScript 的按位操作符又是将其操作数当作 32 位比特序列的。

那么同一个应用下可用的权限数就非常有限了。

这也是该方案的局限性。

四.权限空间

基于权限空间,我们定义两个格式:

1.权限 code

形如 index,pos。其中 pos 表示 32 位二进制数中 1 的位置(其余全是 0); index 表示权限空间,用于突破 JavaScript 数字位数的限制,是从 0 开始的正整数,每个权限code都要归属于一个权限空间。index 和 pos 使用英文逗号隔开。

2.用户权限

字符串,形如 1,16,16。英文逗号分隔每一个权限空间的权限值。例如 1,16,16 的意思就是,权限空间 0 的权限值是 1,权限空间 1 的权限值是 16,权限空间 2 的权限是 16。

引入权限空间的概念突破二进制运算的位数限制。

五.在实际项目的应用

1.定义权限码

// 销售中心
export const Sell_Permission_Data = {
  SELL_INTENTION_INDEX: {
    value: "3,0",
    label: "意向用户列表"
  },
  SELL_INTENTION_ADD: {
    value: "3,1",
    label: "添加意向用户"
  },
  SELL_INTENTION_INFO: {
    value: "3,2",
    label: "添加意向查看"
  },
  SELL_INTENTION_FOLLOW: {
    value: "3,3",
    label: "添加意向跟进"
  },
  SELL_INTENTION_SIGN: {
    value: "3,4",
    label: "添加意向报名"
  },
  SELL_INTENTION_EDIT: {
    value: "3,5",
    label: "添加意向用户"
  },
  SELL_INTENTION_DELETE: {
    value: "3,6",
    label: "添加意向用户"
  }
}

2.权限涉及操作

import { PermissionItem } from './permission.class';
import { UserService } from '../../@core/user/user.service';

export class PermissionService {

  // 权限 code 即用户在本系统的权限码
  public static userCode: string = "";

  private permissions: any = {};

  get userService(): UserService {

    return this.injector.get(UserService);
  }


   // 获取用户权限码
  public getUserPermissionCode() {

    if (PermissionService.userCode) {
      return PermissionService.userCode;
    }

    const user = this.userService.read() || {};
    const code = user.code || '';
    if (!PermissionService.userCode) PermissionService.userCode = code;

    return code;
  }



  // 赋权
  public addPermission(permission: PermissionItem): void {

    const userPermission: any = PermissionService.userCode ? PermissionService.userCode.split(",") : [];
    const [index, pos] = permission.value.split(",");
    const _index: number = parseInt(index);
    const _pos: number = parseInt(pos);
    userPermission[_index] = (userPermission[_index] || 0) | Math.pow(2, _pos)

    PermissionService.userCode = userPermission.join(",");
  }

   // 删权
  public delPermission(permission: PermissionItem): void {

    const userPermission: any = PermissionService.userCode ? PermissionService.userCode.split(",") : [];
    const [index, pos] = permission.value.split(",");
    const _index: number = parseInt(index);
    const _pos: number = parseInt(pos);

    userPermission[_index] = (userPermission[_index] || 0) & (~Math.pow(2, _pos))

    PermissionService.userCode = userPermission.join(",");
  }

  // 鉴权
  public hasPermission(permission: PermissionItem): boolean {

    const userPermission: any = PermissionService.userCode ? PermissionService.userCode.split(",") : [];
    const [index, pos] = permission.value.split(",");
    const _index: number = parseInt(index);
    const _pos: number = parseInt(pos);
    const permissionValue = Math.pow(2, _pos)

    return (userPermission[_index] & permissionValue) === permissionValue;
  }

  // 用户已有权限列表
  public listPermission(): Array<string> {

    const results: Array<string> = [];

    if (!PermissionService.userCode) return results;

    Object.values(this.permissions).forEach((permission: any) => {
      this.hasPermission(permission) && results.push(permission.label);
    });

    return results;
  }

  // 已拥有权限权值
  public valuePermission(): Array<string> {

    const results: Array<string> = [];

    if (!PermissionService.userCode) return results;

    Object.values(this.permissions).forEach((permission: any) => {
      this.hasPermission(permission) && results.push(permission.value);
    });

    return results;
  }

}

六.结合实际场景应用

前端的权限控制可以减少很大程度上的后端压力,一个复杂的系统如果每个模块的权限设计都采用“用户-角色-权限-角色权限-用户角色”的方式去设计,单是权限模块将会非常复杂,

而且这类控制型操作权限请求频率又高,长期以往系统将会更加复杂且难维护,进制转换不但节省大量空间又减少了请求查询。在我们的项目中如果存在很多交叉状态的控制,可以考虑此方案的可行性,

并结合后端权限模型可以实现更多丰富的可用操作。