4. ACL核心算法:权限检查、合并与继承

ACL 这东西,说白了就是一套「谁能不能干什么」的规则。但规则多了,怎么高效判断?怎么处理冲突?怎么让权限自动往下传?这就是今天要聊的三个核心算法。

我刚开始接触 ACL 时,觉得不就是查个表嘛。后来在项目中遇到一个用户既有部门角色又有项目角色,权限叠在一起,结果该有的功能打不开,不该有的反而能访问……嗯,从那以后我才认真研究起这些算法来。

4.1 权限检查算法

权限检查是 ACL 最基础的操作。用户发起一个请求,系统要回答:「这人能访问这个资源吗?」

算法其实不复杂,核心就三步:

  1. 定位:找到用户对应的所有 ACL 条目(直接授予的、通过角色获得的)
  2. 匹配:检查这些条目中是否包含目标资源
  3. 裁决:根据匹配结果返回 允许/拒绝

关键原则:大多数 ACL 实现采用「显式拒绝优先」策略。也就是说,只要有一条 ACL 条目明确拒绝了该操作,不管有多少条允许,最终结果都是拒绝。

我习惯把检查算法写成这样:

function checkAccess(user, resource, operation):
    // 1. 收集用户所有 ACL 条目
    entries = getACLEntries(user)
    
    // 2. 先看有没有显式拒绝
    for entry in entries:
        if entry.resource == resource 
           and entry.operation == operation
           and entry.effect == DENY:
            return DENY  // 一票否决
    
    // 3. 再看有没有显式允许
    for entry in entries:
        if entry.resource == resource 
           and entry.operation == operation
           and entry.effect == ALLOW:
            return ALLOW
    
    // 4. 默认拒绝
    return DENY

避坑指南:我曾经在项目中忘了处理「通配符资源」的情况。比如用户对 /project/* 有读权限,但代码只精确匹配 /project/123。结果用户死活打不开项目详情页。记得加上前缀匹配或正则匹配的逻辑。

4.2 ACL 合并算法

一个用户往往有多个身份。比如在公司里,他既是「研发部成员」,又是「项目A管理员」。这两个身份可能对同一个资源有不同的权限设置。怎么合并?

合并算法要解决的就是「多源权限冲突」的问题。常见的策略有三种:

策略 规则 适用场景
拒绝优先 任何来源的 DENY 都覆盖 ALLOW 安全敏感系统
允许优先 任何来源的 ALLOW 都覆盖 DENY 协作开放系统
最具体优先 粒度更细的条目优先级更高 复杂层级组织

我个人推荐「拒绝优先 + 最具体优先」的组合策略。说白了就是:

  • 先按资源匹配的精确度排序
  • 同精度下,拒绝优先

举个例子:

用户A的权限来源:
- 角色「研发部」:允许读取 /project/*
- 角色「项目B成员」:拒绝读取 /project/B

合并结果:
- 访问 /project/A → 允许(只有研发部规则匹配)
- 访问 /project/B → 拒绝(项目B的规则更具体,且拒绝优先)

注意:合并算法一定要考虑性能。如果每次请求都把所有 ACL 条目拉出来排序,系统扛不住的。我建议在用户登录或角色变更时,提前计算好合并后的权限快照,存到缓存里。

4.3 ACL 继承算法

继承解决的是「权限传递」问题。比如你给一个文件夹设置了权限,里面的文件应该自动继承。不然每个文件都要单独配,那不得累死?

继承算法的核心逻辑是:

  1. 向上查找:如果当前资源没有明确的 ACL 条目,就往父节点找
  2. 逐层递归:一直找到根节点为止
  3. 就近覆盖:子节点如果有自己的设置,就覆盖继承来的

我画了一张图,帮你理解这个流程:

ACL 继承算法流程图 根目录 / 父目录 /docs 子文件 /docs/report.pdf 继承规则 1. 子节点无ACL → 继承父节点 2. 子节点有ACL → 覆盖父节点 3. 逐层向上,直到根节点 当前节点有ACL? 否 → 向上查找 是 → 直接使用 实际项目中的继承策略 • 文件系统:子文件继承父目录权限,但可单独覆盖 • 组织架构:下级部门继承上级部门权限,但可额外添加

代码实现也不复杂:

function getEffectiveACL(resource, user):
    // 先看当前资源有没有直接 ACL
    acl = getDirectACL(resource, user)
    if acl != null:
        return acl
    
    // 没有就找父节点
    parent = getParent(resource)
    if parent == null:
        return DEFAULT_DENY  // 到根了还没找到
    
    // 递归向上
    return getEffectiveACL(parent, user)

避坑指南:我曾经在项目中遇到一个性能问题——文件层级很深,每次访问都要递归几十层。后来加了「路径缓存」,把每个节点的继承结果缓存起来,只有 ACL 变更时才清除缓存。性能提升了 10 倍不止。

4.4 三种算法的配合使用

在实际系统中,这三个算法不是孤立的。它们通常这样配合:

  1. 继承算法先算出用户对某个资源的「有效 ACL 列表」
  2. 合并算法把多个来源的 ACL 合并成一份
  3. 检查算法用合并后的结果做最终裁决

你想想看,如果没有继承,每个文件都要配权限,维护成本太高。如果没有合并,多角色用户就乱套了。如果没有检查算法,那 ACL 就是个摆设。

我的建议:设计 ACL 系统时,先把这三个算法的接口定义清楚。接口稳定了,具体实现可以慢慢优化。我在项目中吃过亏——一上来就写具体逻辑,结果后面要加缓存、加审计日志,改得想哭。