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 支持多层递归继承,我个人建议最多两层。继承链太长,排查问题会让你崩溃。
  • 拒绝优先:无论继承还是显式,只要有一条规则是拒绝,且主体匹配,就直接拒绝。
核心原则: ACL 的最终决策 = 所有匹配规则的并集,但拒绝规则具有「一票否决权」。

4.3.2 优先级判定流程

  1. 收集所有匹配主体的规则(包括显式和继承的)。
  2. 按拒绝/允许分组,拒绝组先判断。
  3. 拒绝组中,显式拒绝优先级高于继承拒绝。
  4. 如果没有匹配的拒绝规则,则检查允许组。
  5. 允许组中,显式允许优先级高于继承允许。
  6. 如果没有任何匹配的允许规则,默认拒绝。

这个流程我写死为一张图放在下面,你品一下:

ACL 决策流程图 收到访问请求 收集所有匹配规则 检查拒绝规则(显式 > 继承) 有拒绝匹配 → 直接拒绝 无拒绝 → 检查允许规则 拒绝访问 检查允许规则(显式 > 继承) 无任何允许 → 默认拒绝 允许匹配 → 允许访问 注:显式规则的优先级始终高于继承规则;拒绝规则拥有一票否决权。

这张图你应该能一目了然。我特别强调一下「默认拒绝」:任何 ACL 体系的最后一道防线就是「没有匹配则拒绝」。千万别图省事搞「默认允许」——你想想,一个权限漏配就等于裸奔。

4.4 实战中的继承与优先级陷阱

有一次我在做某个文件共享 SDK 时,用户 A 在父目录给了子目录「只读」的继承规则,然后又在子目录里给另一个用户 B 单独写了「读写」的显式规则。按照我们的优先级逻辑:显式规则 > 继承规则,所以 B 拿到读写是没问题的。但是问题出在「继承标志」上——子目录没有正确传递继承标志给它的子文件,导致某些深层文件既没继承父目录的只读规则,也没显式规则,直接默认拒绝了。这就是典型的「继承断链」问题。

避坑指南: 实现 ACL 继承时,一定要在遍历树时携带「祖先继承标志」的状态,而不是只检查父节点一层。我曾用递归参数传递 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 权限管理的基石。结构上牢牢记住「主体 + 权限位 + 继承标志」三元组;权限位用位掩码定义,别乱放拒绝标志;继承与优先级的关键就是「显式 > 继承」和「拒绝一票否决」。你把这个流程用代码固化下来,后面所有的权限判断都会清晰很多。