4. 访问控制列表(ACL):结构、权限位与继承优先级
ACL 这东西,说白了就是给每个资源挂一张「准入名单」。我在早期对接某个第三方 SDK 时吃过亏——对方文档里写着「支持 ACL」,结果实际实现就一个全量放行的空壳。后来我养成了习惯:拿到 SDK 第一件事就是拆它的 ACL 结构。
4.1 ACL 结构
一个完整的 ACL 记录,通常由三部分组成:主体、权限、继承标志。嗯,你可以把它理解成一张小卡片:
- 主体:谁在请求?可以是 App 包名、UID、证书指纹,或者一个角色标签。
- 权限位:允许做什么?读、写、执行、审计等。
- 继承标志:这条规则要不要往下传给子对象?
我习惯用下面这个结构体来表示:
struct acl_entry {
char subject[64]; // 主体标识
uint32_t permissions; // 权限位掩码
uint8_t inherit_flags; // 继承控制位
uint8_t entry_type; // 允许/拒绝
};
注意,这里 entry_type 是个容易被忽略的字段。我在项目里见过一个坑:把拒绝规则和允许规则混在同一条里,结果判断逻辑炸了。ACL 最好的做法是「先遍历拒绝,再遍历允许」,后面我会细说。
4.2 权限位定义
权限位,就是用二进制位来代表不同操作。为什么这么设计?你想啊,一个资源同时要支持多种操作,用位掩码做位运算最快,也最省空间。
我给一套通用定义,你可以在自己的 SDK 里直接复用:
| 权限名 | 位位置 | 掩码值 | 说明 |
|---|---|---|---|
| PERM_READ | 0 | 0x01 | 读取资源内容 |
| PERM_WRITE | 1 | 0x02 | 修改资源内容 |
| PERM_EXEC | 2 | 0x04 | 执行资源上的操作 |
| PERM_DELETE | 3 | 0x08 | 删除资源 |
| PERM_SHARE | 4 | 0x10 | 允许将资源分享给其他主体 |
| PERM_AUDIT | 5 | 0x20 | 审计跟踪操作日志 |
举个例子,如果一个 ACL 条目允许读和写,权限位就是 0x01 | 0x02 = 0x03。判断时只要 requested & entry->permissions == requested 就能通过,非常快。
entry_type 独立表示。否则你会在权限运算里陷入「允许读但拒绝写」之类的两难逻辑。我曾经在一个项目里看到过这种设计,最后不得不重构整个 ACL 引擎。
4.3 ACL 继承与优先级
继承,解决的是「懒惰」问题。一个目录下有几百个文件,难道每个文件都要写一遍 ACL?当然不。继承让子对象自动继承父对象的 ACL 规则。
但继承有个核心难题:父子规则冲突怎么办?这就轮到优先级出场了。
4.3.1 继承规则
- 显式规则 > 继承规则:子对象自己定义的 ACL 条目,优先级总是高于从父对象继承来的条目。
- 只向下继承一层:有些 SDK 支持多层递归继承,我个人建议最多两层。继承链太长,排查问题会让你崩溃。
- 拒绝优先:无论继承还是显式,只要有一条规则是拒绝,且主体匹配,就直接拒绝。
4.3.2 优先级判定流程
- 收集所有匹配主体的规则(包括显式和继承的)。
- 按拒绝/允许分组,拒绝组先判断。
- 拒绝组中,显式拒绝优先级高于继承拒绝。
- 如果没有匹配的拒绝规则,则检查允许组。
- 允许组中,显式允许优先级高于继承允许。
- 如果没有任何匹配的允许规则,默认拒绝。
这个流程我写死为一张图放在下面,你品一下:
这张图你应该能一目了然。我特别强调一下「默认拒绝」:任何 ACL 体系的最后一道防线就是「没有匹配则拒绝」。千万别图省事搞「默认允许」——你想想,一个权限漏配就等于裸奔。
4.4 实战中的继承与优先级陷阱
有一次我在做某个文件共享 SDK 时,用户 A 在父目录给了子目录「只读」的继承规则,然后又在子目录里给另一个用户 B 单独写了「读写」的显式规则。按照我们的优先级逻辑:显式规则 > 继承规则,所以 B 拿到读写是没问题的。但是问题出在「继承标志」上——子目录没有正确传递继承标志给它的子文件,导致某些深层文件既没继承父目录的只读规则,也没显式规则,直接默认拒绝了。这就是典型的「继承断链」问题。
inherited_flags 来避免这个问题。
4.5 代码示例:ACL 决策函数
下面是一个简化但可用的决策函数核心逻辑:
bool acl_check(struct acl_entry *entries, int count,
const char *subject, uint32_t requested_perms) {
// 第一步:检查拒绝规则
for (int i = 0; i < count; i++) {
if (entries[i].entry_type != ACL_DENY) continue;
if (strcmp(entries[i].subject, subject) != 0) continue;
// 拒绝规则匹配,直接返回false
return false;
}
// 第二步:检查允许规则
for (int i = 0; i < count; i++) {
if (entries[i].entry_type != ACL_ALLOW) continue;
if (strcmp(entries[i].subject, subject) != 0) continue;
if ((requested_perms & entries[i].permissions) == requested_perms) {
return true; // 所有请求的权限都被允许
}
}
// 第三步:默认拒绝
return false;
}
这个代码里我故意没加继承逻辑——因为继承需要递归遍历父节点,代码会变得很臃肿。但核心决策思想不变:拒绝优先、显式优先、默认拒绝。
我个人的习惯:在每个 ACL 条目里额外记录一个 depth 字段。显式规则的 depth = 0,继承规则的 depth 就是层级数。这样在比较优先级时,depth 越小的规则越优先,简单直观。
4.6 小结
ACL 是隐私共享 SDK 权限管理的基石。结构上牢牢记住「主体 + 权限位 + 继承标志」三元组;权限位用位掩码定义,别乱放拒绝标志;继承与优先级的关键就是「显式 > 继承」和「拒绝一票否决」。你把这个流程用代码固化下来,后面所有的权限判断都会清晰很多。