文章

Linux 数字权限设计方法

Linux 数字权限设计方法

Linux 权限的数字表示法

在 Linux 里,每个文件和目录都有权限设置,控制谁能对它做什么。权限分三档:读、写、执行,用数字来编码之后,配置起来非常直观。

三种基本权限对应的数字

  • 读(read)— 数字 4,可以查看文件内容,对目录来说就是能 ls 列出文件
  • 写(write)— 数字 2,可以修改文件内容,对目录来说就是能在里面创建、删除文件
  • 执行(execute)— 数字 1,可以运行文件作为程序,对目录来说就是能 cd 进入

0 表示没有任何权限。

为什么偏偏选 4、2、1 这三个数?因为它们各自是 2 的幂次(2²、2¹、2⁰),对应二进制中的三个独立位。每个权限独占一个 bit,加起来的结果不会产生歧义。不存在”5到底是4+1还是3+2”的困惑,因为 3 本身就不是一个基础权限位,它已经是 2+1 的组合。这套设计的精髓就在于:任意组合的求和结果在 0-7 之间都是唯一的,不会出现两种不同权限组合得到同一个数字的情况。

用二进制来看就非常直观,每个 bit 对应一种权限,1 表示有,0 表示没有:

十进制二进制读(4)写(2)执行(1)含义
0000000无权限
1001001只执行
2010010只写
3011011写 + 执行
4100100只读
5101101读 + 执行
6110110读 + 写
7111111读 + 写 + 执行

5 那行,二进制 101——读那列是 1,写那列是 0,执行那列是 1,一目了然,没有任何歧义的余地。

所以 chmod 755 翻译成二进制就是:

1
2
3
所有者: 7 = 111  (读 + 写 + 执行)
同  组: 5 = 101  (读 + 执行)
其  他: 5 = 101  (读 + 执行)

三类用户身份

Linux 里权限是按用户身份分开设置的,一共有三类:

  • 所有者(owner) — 文件的拥有者,通常是创建它的人
  • 同组用户(group) — 和所有者同属一个用户组的其他人
  • 其他用户(other) — 既不是所有者也不在同一个组的其他人

所以一个完整的权限需要给三类身份各分配一个数字,写成三位数。比如 chmod 755 的意思是:

  • 所有者:7(读 + 写 + 执行)
  • 同组用户:5(读 + 执行)
  • 其他用户:5(读 + 执行)

常见的 chmod 用法

日常开发里用得最多的几个权限组合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 脚本文件,所有人都能执行,但只有所有者能改
chmod 755 deploy.sh

# 配置文件,只有所有者能读写,其他人完全无权限
chmod 600 .env

# 普通文档,所有者可读写,其他人只读
chmod 644 README.md

# 目录,所有人都能进入和查看,但只有所有者能增删文件
chmod 755 src/

# 递归修改目录下所有文件的权限
chmod -R 644 docs/

用 ls -l 查看权限

执行 ls -l 看到的 -rwxr-xr-x 其实就是数字权限的字母版:

1
2
3
4
5
6
-rwxr-xr-x  1 pe7er  staff  deploy.sh
│├─┤├─┤├─┤
│ │  │  └── other: r-x = 5
│ │  └───── group:  r-x = 5
│ └──────── owner:  rwx = 7
└────────── 文件类型(- 普通文件,d 目录,l 链接)

所以 -rwxr-xr-x 对应的数字就是 755

几个容易踩的坑

不要图省事直接 chmod 777777 意味着所有人都能随便改你的文件,在生产服务器上这样做等于开了个安全漏洞。绝大多数场景下,755 给目录、644 给文件就够了。

.env、私钥文件这类敏感配置,权限一定要设成 600 甚至 400,只让自己能读。很多工具(比如 SSH)检测到私钥权限太宽松会直接拒绝工作。

chmod -R 递归修改的时候要注意,目录和文件的合理权限不一样——目录通常需要 755(需要执行权限才能进入),文件通常 644 就够。可以用 find 分开处理:

1
2
3
# 目录设 755,文件设 644
find . -type d -exec chmod 755 {} \;
find . -type f -exec chmod 644 {} \;

速查表

权限值所有者同组其他适用场景
755rwxr-xr-x可执行脚本、目录
644rw-r–r–普通文件、配置文件
600rw-敏感文件(密钥、.env)
700rwx私有脚本或目录
400r–只读敏感文件

位掩码权限设计在应用系统中的落地

Linux 用三个 bit 表示三种权限,这个思路搬到业务系统里一样好使,而且比一堆布尔字段或字符串数组要干净得多。

基本思路:每个权限占一个 bit

假设一个订单模块有这些操作权限:

权限二进制位十进制值
查看订单00011
创建订单00102
编辑订单01004
删除订单10008

和 Linux 的设计一模一样——每个权限独占一个 bit,值是 2 的幂次。

用位运算组合和判断权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 定义权限常量
const PERM = {
  VIEW:   1 << 0,  // 0001 = 1
  CREATE: 1 << 1,  // 0010 = 2
  EDIT:   1 << 2,  // 0100 = 4
  DELETE: 1 << 3,  // 1000 = 8
}

// 给角色分配权限:用 | (按位或) 组合多个权限
const adminPerm  = PERM.VIEW | PERM.CREATE | PERM.EDIT | PERM.DELETE  // 1111 = 15
const editorPerm = PERM.VIEW | PERM.CREATE | PERM.EDIT                // 0111 = 7
const viewerPerm = PERM.VIEW                                          // 0001 = 1

// 判断角色是否有某个权限:用 & (按位与)
function hasPermission(rolePerm: number, required: number): boolean {
  return (rolePerm & required) === required
}

hasPermission(editorPerm, PERM.EDIT)    // true  (0111 & 0100 = 0100)
hasPermission(editorPerm, PERM.DELETE)  // false (0111 & 1000 = 0000)
hasPermission(adminPerm, PERM.DELETE)   // true  (1111 & 1000 = 1000)

1 << n 是位运算的左移操作,把 1 往左移 n 位,效果就是 2 的 n 次方。写法比直接写 1, 2, 4, 8 直观,而且新增权限时只需要加一行 1 << 4,不用自己算下一个 2 的幂是多少。

多模块权限:用多段 bit 拼

实际业务里往往不止一个模块。订单有增删改查,商品也有增删改查。可以按模块分段,每 4 个 bit 一个模块:

1
2
3
4
5
           商品模块    订单模块
           ----        ----
管理员:    1111        1111   = 0xFF
运营:      1110        0111   = 0xE7
客服:      0001        0001   = 0x11
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const ORDER_VIEW   = 1 << 0
const ORDER_CREATE = 1 << 1
const ORDER_EDIT   = 1 << 2
const ORDER_DELETE = 1 << 3

const PRODUCT_VIEW   = 1 << 4
const PRODUCT_CREATE = 1 << 5
const PRODUCT_EDIT   = 1 << 6
const PRODUCT_DELETE = 1 << 7

// 运营角色:订单可增改查(0111),商品可增改查但不可删(1110)
const opsPerm = ORDER_VIEW | ORDER_CREATE | ORDER_EDIT
              | PRODUCT_VIEW | PRODUCT_CREATE | PRODUCT_EDIT
// = 0111 0111 = 0x77

hasPermission(opsPerm, ORDER_DELETE)    // false
hasPermission(opsPerm, PRODUCT_EDIT)    // true

一个整数就能表达两个模块、八种权限的完整组合,存数据库只要一个字段。

和数据库、接口的配合

数据库层面,角色的权限就是一个整数:

1
2
3
4
5
CREATE TABLE roles (
  id          INT PRIMARY KEY,
  name        VARCHAR(50),
  permissions INT NOT NULL DEFAULT 0
)

后端接口做鉴权的时候,一行位运算就搞定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Express/Koa 中间件示例
function requirePermission(required: number) {
  return (req, res, next) => {
    const rolePerm = req.user.rolePermissions  // 从 session/JWT 里取
    if ((rolePerm & required) !== required) {
      return res.status(403).json({ message: '权限不足' })
    }
    next()
  }
}

app.delete('/api/orders/:id',
  requirePermission(PERM.DELETE),
  orderController.delete
)

前端路由守卫也用同样的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const routes = [
  {
    path: '/orders',
    component: OrderList,
    meta: { permission: PERM.VIEW }
  },
  {
    path: '/orders/create',
    component: OrderCreate,
    meta: { permission: PERM.CREATE }
  }
]

router.beforeEach((to) => {
  const required = to.meta.permission
  if (required && !hasPermission(userStore.permissions, required)) {
    return { path: '/403' }
  }
})

位掩码方案的边界

这套方案在权限数量不多的时候非常高效,但也有它的适用范围。权限数量在 32 个以内(JS 的位运算上限),一个整数就够用,性能和存储都是最优解。一旦权限超过 32 种,就需要用 BigInt 或者拆成多个字段,这时候不如直接换成权限列表或 RBAC 权限表来管理。

大多数中小业务系统,十几个权限点用位掩码绰绰有余,而且代码里一眼就能看出某个角色到底有哪些权限——把整数转成二进制,哪个 bit 是 1 就对应哪个权限,比翻一堆布尔字段清晰得多。

写在最后

以上内容只是提供一种思路,帮助理解位掩码在权限设计中的工作原理和优势。切勿随意在生产环境方案中照搬使用——实际业务场景往往比示例复杂得多,盲目套用位掩码方案可能会因为错误的使用方式,反而增加代码逻辑复杂度和团队的心智负担。选型之前,先评估业务的权限规模和团队协作成本,合适的方案才是最好的方案。

延伸阅读

位掩码的设计思想在很多看似不相关的领域都有应用。经典面试题”老鼠试毒药”就是同一个思路:每只老鼠代表二进制的一个 bit 位,通过死活状态的组合来定位毒药瓶,本质上就是用 n 个 bit 区分 2^n 种情况。

网络领域的子网掩码也是位掩码的经典应用。255.255.255.0 写成二进制就是 11111111.11111111.11111111.00000000,用 1 的位标识网络部分,用 0 的位标识主机部分,通过位运算 AND 就能快速判断两台机器是否在同一个子网。

这三者的底层逻辑完全相通:用最少的 bit 表达最多的状态,用位运算快速提取或判断。感兴趣可以对照本站的《经典面试题:老鼠试毒药》《IP 地址:从 IPv4 到 IPv6》两篇文章,体会掩码设计在不同场景下的应用。

本文由作者按照 CC BY 4.0 进行授权