zoukankan      html  css  js  c++  java
  • 【JS】413- JavaScript中的位运算和权限设计

    点击上方“前端自习课”关注,学习起来~

    1. 内容概要

    本文主要讨论以下两个问题:

    • JavaScript 的位运算:先简单回顾下位运算,平时用的少,相信不少人和我一样忘的差不多了

    • 权限设计:根据位运算的特点,设计一个权限系统(添加、删除、判断等)

    2. JavaScript 位运算

    2.1. Number

    在讲位运算之前,首先简单看下 JavaScript 中的 Number,下文需要用到。在 JavaScript 里,数字均为基于 IEEE 754 标准的双精度 64 位的浮点数,引用维基百科的图片,它的结构长这样: 

    • sign bit(符号): 用来表示正负号

    • exponent(指数): 用来表示次方数

    • mantissa(尾数): 用来表示精确度

    也就是说一个数字的范围只能在 -(2^53 -1) 至 2^53 -1 之间。

    既然讲到这里,就多说一句:0.1 + 0.2 算不准的原因也在于此。浮点数用二进制表达时是无穷的,且最多 53 位,必须截断,进而产生误差。最简单的解决办法就是放大一定倍数变成整数,计算完成后再缩小。不过更稳妥的办法是使用下文将会提到的 math.js 等工具库。

    此外还有四种数字进制:

    1. // 十进制

    2. 123456789

    3. 0

    4. // 二进制:前缀 0b,0B

    5. 0b10000000000000000000000000000000 // 2147483648

    6. 0b01111111100000000000000000000000 // 2139095040

    7. 0B00000000011111111111111111111111 // 8388607

    8. // 八进制:前缀 0o,0O(以前支持前缀 0)

    9. 0o755 // 493

    10. 0o644 // 420

    11. // 十六进制:前缀 0x,0X

    12. 0xFFFFFFFFFFFFFFFFF // 295147905179352830000

    13. 0x123456789ABCDEF // 81985529216486900

    14. 0XA // 10

    15. 复制代码

    好了,Number 就说这么多,接下来看 JavaScript 中的位运算。

    2.2. 位运算

    按位操作符将其操作数当作 32 位的比特序列(由 0 和 1 组成)操作,返回值依然是标准的 JavaScript 数值。JavaScript 中的按位操作符有:

    下面举几个例子,主要看下 AND 和 OR

    1. # 例子1

    2. A = 10001001

    3. B = 10010000

    4. A | B = 10011001

    5. # 例子2

    6. A = 10001001

    7. C = 10001000

    8. A | C = 10001001

    9. 复制代码

    1. # 例子1

    2. A = 10001001

    3. B = 10010000

    4. A & B = 10000000

    5. # 例子2

    6. A = 10001001

    7. C = 10001000

    8. A & C = 10001000

    9. 复制代码

    3. 位运算在权限系统中的使用

    传统的权限系统里,存在很多关联关系,如用户和权限的关联,用户和角色的关联。系统越大,关联关系越多,越难以维护。而引入位运算,可以巧妙的解决该问题。在讲“位运算在权限系统中的使用”之前,我们先假定两个前提,下文所有的讨论都是基于这两个前提的

    1. 每种权限码都是唯一的(这是显然的)

    2. 所有权限码的二进制数形式,有且只有一位值为 1,其余全部为 0( 2^n

    如果用户权限和权限码,全部使用二级制数字表示,再结合上面 AND 和 OR 的例子,分析位运算的特点,不难发现:

    • | 可以用来赋予权限

    • & 可以用来校验权限

    为了讲的更明白,这里用 Linux 中的实例分析下,Linux 的文件权限分为读、写和执行,有字母和数字等多种表现形式:

    可以看到,权限用 1、2、4(也就是 2^n)表示,转换为二进制后,都是只有一位是 1,其余为 0。我们通过几个例子看下,如何利用二进制的特点执行权限的添加,校验和删除。

    3.1. 添加权限

    1. let r = 0b100

    2. let w = 0b010

    3. let x = 0b001

    4. // 给用户赋全部权限(使用前面讲的 | 操作)

    5. let user = r | w | x

    6. console.log(user)

    7. // 7

    8. console.log(user.toString(2))

    9. // 111

    10. // r = 0b100

    11. // w = 0b010

    12. // r = 0b001

    13. // r|w|x = 0b111

    14. 复制代码

    可以看到,执行 r|w|x 后, user 的三位都是 1,表明拥有了全部三个权限。

    Linux 下出现权限问题时,最粗暴的解决方案就是 chmod777xxx,这里的 7 就代表了:可读,可写,可执行。而三个 7 分别代表:文件所有者,文件所有者所在组,所有其他用户。

    3.2. 校验权限

    刚才演示了权限的添加,下面演示权限校验:

    1. let r = 0b100

    2. let w = 0b010

    3. let x = 0b001

    4. // 给用户赋 r w 两个权限

    5. let user = r | w

    6. // user = 6

    7. // user = 0b110 (二进制)

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

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

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

    11. 复制代码

    如前所料,通过 用户权限&权限code===权限code 就可以判断出用户是否拥有该权限。

    3.3. 删除权限

    我们讲了用 | 赋予权限,使用 & 判断权限,那么删除权限呢?删除权限的本质其实是将指定位置上的 1 重置为 0。上个例子里用户权限是 0b110,拥有读和写两个权限,现在想删除读的权限,本质上就是将第三位的 1 重置为 0,变为 0b010

    1. let r = 0b100

    2. let w = 0b010

    3. let x = 0b001

    4. let user = 0b010;

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

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

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

    8. 复制代码

    那么具体怎么操作呢?其实有两种方案,最简单的就是异或 ^,按照上文的介绍“当两个操作数相应的比特位有且只有一个 1 时,结果为 1,否则为 0”,所以异或其实是 toggle 操作,无则增,有则减:

    1. let r = 0b100

    2. let w = 0b010

    3. let x = 0b001

    4. let user = 0b110 // 有 r w 两个权限

    5. // 执行异或操作,删除 r 权限

    6. user = user ^ r

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

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

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

    10. console.log(user.toString(2)) // 现在 user 是 0b010

    11. // 再执行一次异或操作

    12. user = user ^ r

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

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

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

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

    17. 复制代码

    那么如果单纯的想删除权限(而不是无则增,有则减)怎么办呢?答案是执行 &(~code),先取反,再执行与操作:

    1. let r = 0b100

    2. let w = 0b010

    3. let x = 0b001

    4. let user = 0b110 // 有 r w 两个权限

    5. // 删除 r 权限

    6. user = user & (~r)

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

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

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

    10. console.log(user.toString(2)) // 现在 user 是 0b010

    11. // 再执行一次

    12. user = user & (~r)

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

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

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

    16. console.log(user.toString(2)) // 现在 user 还是 0b010,并不会新增

    17. 复制代码

    4. 局限性和解决办法

    前面我们回顾了 JavaScript 中的 Number 和位运算,并且了解了基于位运算的权限系统原理和 Linux 文件系统权限的实例。上述的所有都有前提条件: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. // 用户的权限 code

    2. let userCode = ""

    3. // 假设系统里有这些权限

    4. // 纯模拟,正常情况下是按顺序的,如 0,0 0,1 0,2 ...,尽可能占满一个权限空间,再使用下一个

    5. const permissions = {

    6. SYS_SETTING: {

    7. value: "0,0", // index = 0, pos = 0

    8. info: "系统权限"

    9. },

    10. DATA_ADMIN: {

    11. value: "0,8",

    12. info: "数据库权限"

    13. },

    14. USER_ADD: {

    15. value: "0,22",

    16. info: "用户新增权限"

    17. },

    18. USER_EDIT: {

    19. value: "0,30",

    20. info: "用户编辑权限"

    21. },

    22. USER_VIEW: {

    23. value: "1,2", // index = 1, pos = 2

    24. info: "用户查看权限"

    25. },

    26. USER_DELETE: {

    27. value: "1,17",

    28. info: "用户删除权限"

    29. },

    30. POST_ADD: {

    31. value: "1,28",

    32. info: "文章新增权限"

    33. },

    34. POST_EDIT: {

    35. value: "2,4",

    36. info: "文章编辑权限"

    37. },

    38. POST_VIEW: {

    39. value: "2,19",

    40. info: "文章查看权限"

    41. },

    42. POST_DELETE: {

    43. value: "2,26",

    44. info: "文章删除权限"

    45. }

    46. }

    47. // 添加权限

    48. const addPermission = (userCode, permission) => {

    49. const userPermission = userCode ? userCode.split(",") : []

    50. const [index, pos] = permission.value.split(",")

    51. userPermission[index] = (userPermission[index] || 0) | Math.pow(2, pos)

    52. return userPermission.join(",")

    53. }

    54. // 删除权限

    55. const delPermission = (userCode, permission) => {

    56. const userPermission = userCode ? userCode.split(",") : []

    57. const [index, pos] = permission.value.split(",")

    58. userPermission[index] = (userPermission[index] || 0) & (~Math.pow(2, pos))

    59. return userPermission.join(",")

    60. }

    61. // 判断是否有权限

    62. const hasPermission = (userCode, permission) => {

    63. const userPermission = userCode ? userCode.split(",") : []

    64. const [index, pos] = permission.value.split(",")

    65. const permissionValue = Math.pow(2, pos)

    66. return (userPermission[index] & permissionValue) === permissionValue

    67. }

    68. // 列出用户拥有的全部权限

    69. const listPermission = userCode => {

    70. const results = []

    71. if (!userCode) {

    72. return results

    73. }

    74. Object.values(permissions).forEach(permission => {

    75. if (hasPermission(userCode, permission)) {

    76. results.push(permission.info)

    77. }

    78. })

    79. return results

    80. }

    81. const log = () => {

    82. console.log(`userCode: ${JSON.stringify(userCode, null, " ")}`)

    83. console.log(`权限列表: ${listPermission(userCode).join("; ")}`)

    84. console.log("")

    85. }

    86. userCode = addPermission(userCode, permissions.SYS_SETTING)

    87. log()

    88. // userCode: "1"

    89. // 权限列表: 系统权限

    90. userCode = addPermission(userCode, permissions.POST_EDIT)

    91. log()

    92. // userCode: "1,,16"

    93. // 权限列表: 系统权限; 文章编辑权限

    94. userCode = addPermission(userCode, permissions.USER_EDIT)

    95. log()

    96. // userCode: "1073741825,,16"

    97. // 权限列表: 系统权限; 用户编辑权限; 文章编辑权限

    98. userCode = addPermission(userCode, permissions.USER_DELETE)

    99. log()

    100. // userCode: "1073741825,131072,16"

    101. // 权限列表: 系统权限; 用户编辑权限; 用户删除权限; 文章编辑权限

    102. userCode = delPermission(userCode, permissions.USER_EDIT)

    103. log()

    104. // userCode: "1,131072,16"

    105. // 权限列表: 系统权限; 用户删除权限; 文章编辑权限

    106. userCode = delPermission(userCode, permissions.USER_EDIT)

    107. log()

    108. // userCode: "1,131072,16"

    109. // 权限列表: 系统权限; 用户删除权限; 文章编辑权限

    110. userCode = delPermission(userCode, permissions.USER_DELETE)

    111. userCode = delPermission(userCode, permissions.SYS_SETTING)

    112. userCode = delPermission(userCode, permissions.POST_EDIT)

    113. log()

    114. // userCode: "0,0,0"

    115. // 权限列表:

    116. userCode = addPermission(userCode, permissions.SYS_SETTING)

    117. log()

    118. // userCode: "1,0,0"

    119. // 权限列表: 系统权限

    120. 复制代码

    除了通过引入权限空间的概念突破二进制运算的位数限制,还可以使用 math.js 的 bignumber,直接运算超过 32 位的二进制数,具体可以看它的文档,这里就不细说了。

    5. 适用场景和问题

    如果按照当前使用最广泛的 RBAC 模型设计权限系统,那么一般会有这么几个实体:应用,权限,角色,用户。用户权限可以直接来自权限,也可以来自角色:

    • 一个应用下有多个权限

    • 权限和角色是多对多的关系

    • 用户和角色是多对多的关系

    • 用户和权限是多对多的关系

    在此种模型下,一般会有用户与权限,用户与角色,角色与权限的对应关系表。想象一个商城后台权限管理系统,可能会有上万,甚至十几万店铺(应用),每个店铺可能会有数十个用户,角色,权限。随着业务的不断发展,刚才提到的那三张对应关系表会越来越大,越来越难以维护。而进制转换的方法则可以省略对应关系表,减少查询,节省空间。当然,省略掉对应关系不是没有坏处的,例如下面几个问题:

    • 如何高效的查找我的权限?

    • 如何高效的查找拥有某权限的所有用户?

    • 如何控制权限的有效期?

    所以进制转换的方案比较适合刚才提到的应用极其多,而每个应用中用户,权限,角色数量较少的场景。

    6. 其他方案

    除了二进制方案,当然还有其他方案可以达到类似的效果,例如直接使用一个1和0组成的字符串,权限点对应index,1表示拥有权限,0表示没有权限。举个例子:添加 0、删除 1、编辑 2,用户A拥有添加和编辑的权限,则 userCode 为 101;用户B拥有全部权限,userCode 为 111。这种方案比二进制转换简单,但是浪费空间。还有利用质数的方案,权限点全部为质数,用户权限为他所拥有的全部权限点的乘积。如:权限点是 2、3、5、7、11,用户权限是 5 * 7 * 11 = 385。这种方案麻烦的地方在于获取质数(新增权限点)和质因数分解(判断权限),权限点特别多的时候就快成 RSA 了,如果只有增删改查个别几个权限,倒是可以考虑。

    7. 参考

    • MDN:JavaScript 数字和日期

    • 双精度浮点类型

    • MDN:按位操作符

    • 【小知识大道理】被忽视的位运算

    • 为什么不要在 JavaScript 中使用位操作符?

    • 角色权限设计的100种解法

    • 权限系统与RBAC模型概述

    • 权限设计及算法

    • 基于角色的访问控制

    原创系列推荐

    1. JavaScript 重温系列(22篇全)

    2. ECMAScript 重温系列(10篇全)

    3. JavaScript设计模式 重温系列(9篇全)

    4. 正则 / 框架 / 算法等 重温系列(16篇全)

    5. Webpack4 入门(上)|| Webpack4 入门(下)

    6. MobX 入门(上) ||  MobX 入门(下)

    7. 59篇原创系列汇总

    回复“加群”与大佬们一起交流学习~

    点这,与大家一起分享本文吧~

    个人博客:http://www.pingan8787.com 微信公众号【前端自习课】和千万网友一起,每日清晨,享受一篇前端优秀文章。 目前已连续推送文章 600+ 天,愿每个人的初心都能一直坚持下去!
  • 相关阅读:
    JZOJ5954.【NOIP2018模拟11.5A组】走向巅峰
    JZOJ5956.【NOIP2018模拟11.7A组】easy LCA
    JZOJ5957.【NOIP2018模拟11.7A组】scarborough fair
    JZOJ5959【NOIP2018模拟11.8A组】铁路运输
    NOIP2018游记
    Pycharm 解释器的快捷键
    计算机编程语言初识
    计算机初识
    Python之如何修改运行的快捷键
    [USACO09NOV]硬币的游戏A Coin Game
  • 原文地址:https://www.cnblogs.com/pingan8787/p/13069610.html
Copyright © 2011-2022 走看看