PAYATHON 2026

如何设计支持权限继承的数据库对象细粒度授权架构?

技术老齐

建议先把业务数据和显式授权存放在同一个关系型数据库中,由服务端统一计算继承权限。对于 Company → Department → Position 这种固定三层结构,带索引的祖先查询通常就适合作为起点,无需仅因担心 JOIN 而引入 LDAP。

授权模型可以采用带继承的对象级 ACL:记录“谁对哪个对象拥有什么权限”,再根据对象的父子关系计算有效权限。以后有多个服务需要共享授权关系时,再评估独立的关系授权服务。

先确定继承与冲突规则

设计数据库结构前,先明确权限冲突时如何处理:用户在 Company 上是 Admin,在某个 Department 上又被授予 Read,最终应该拥有什么权限?

按照题目中“Company 的 Admin 对所有子实体仍是 Admin”的要求,可以采用以下规则:

  • 假设较高级别包含较低级别的能力:Admin > Write > Read。
  • 取本级授权与所有祖先授权中的最高级别。
  • 没有适用的授权时,默认拒绝访问。
  • 授权只向下继承,不自动获得父对象或兄弟对象的权限。
  • 子对象上的低级别授权不会降低继承权限。

对应的计算规则是:

Read  = 1
Write = 2
Admin = 3

effective_level(user, object) =
    max(0, 用户在该对象及所有祖先对象上的显式授权级别)

例如:

Company 授权Department 授权Position 授权Position 有效权限
AdminRead无Admin
无WriteReadWrite
Read无AdminAdmin
无无无无权限

这里假设所有级别都向下继承。如果只允许特定级别继承,还需要细化规则。

如果业务还要求“禁止访问”“停止继承”或“子级覆盖父级”,简单的 max() 就不够了。需要为这些要求单独定义优先级,否则同一组授权数据可能有不同解释。

只存显式授权,保留唯一的对象关系

给 Company 授予 Admin 时,只保存一条 Company 授权记录。Department 和 Position 的继承权限在查询时计算,无需给每个后代复制授权记录。

新增 Position 后,它会继承所属 Department 的权限;撤销祖先授权时,也无需逐个删除后代的权限副本。

以 PostgreSQL 为例,可以用统一的资源表保存对象身份和父子关系:

CREATE TABLE app_user (
    id bigint PRIMARY KEY
);

CREATE TABLE resource (
    id bigint PRIMARY KEY,
    resource_type text NOT NULL
        CHECK (resource_type IN ('Company', 'Department', 'Position')),
    parent_id bigint REFERENCES resource(id),
    CHECK (parent_id IS NULL OR parent_id <> id)
);

CREATE INDEX resource_parent_idx
    ON resource(parent_id);

CREATE TABLE resource_grant (
    user_id bigint NOT NULL REFERENCES app_user(id),
    resource_id bigint NOT NULL
        REFERENCES resource(id) ON DELETE CASCADE,
    permission_level smallint NOT NULL
        CHECK (permission_level BETWEEN 1 AND 3),
    PRIMARY KEY (user_id, resource_id)
);

Company、Department、Position 各自的业务字段仍可存放在独立表中,通过主键关联 resource.id。父子关系以 resource.parent_id 为准,避免多张表各自保存一份可以独立修改的关系。

这份 DDL 只包含授权的基本结构,还需要通过数据库约束、触发器或受控的事务写入逻辑保证:

  • Company 没有父节点。
  • Department 的父节点必须是 Company。
  • Position 的父节点必须是 Department。

parent_id <> id 只能防止节点直接指向自身,无法单独防止多个节点形成循环。还要严格执行上述类型层级规则,才能保证树的结构合法。

如果现有业务表已经通过外键维护了完整关系,可以直接沿用。统一资源表便于统一对象身份与授权关联,是否增加这层抽象,可以根据现有模型决定。

沿祖先链查询有效权限

PostgreSQL 支持用递归 CTE 查询树形关系,具体语法可参考其 WITH 与递归查询文档。

下面的查询从目标对象向上查找祖先,取当前用户的最高授权级别,并判断是否满足所需权限:

-- $1: target resource ID
-- $2: authenticated user ID
-- $3: required level, limited to 1, 2, or 3

WITH RECURSIVE ancestors AS (
    SELECT id, parent_id
    FROM resource
    WHERE id = $1

    UNION ALL

    SELECT parent.id, parent.parent_id
    FROM resource AS parent
    JOIN ancestors AS child
      ON parent.id = child.parent_id
)
SELECT COALESCE(MAX(g.permission_level), 0) >= $3 AS allowed
FROM ancestors AS a
JOIN resource_grant AS g
  ON g.resource_id = a.id
 AND g.user_id = $2;

在合法的三层结构中,查询一个 Position 只需检查 Position、Department、Company 三个节点上的授权,不会遍历 Company 的全部后代。

目标对象不存在或没有适用授权时,查询返回 false。用户 ID 应来自服务端验证过的身份,所需权限则由接口执行的操作确定,不能由客户端自行决定。

如果层级始终只有三层,也可以用固定 JOIN 或 UNION ALL 构造祖先集合。是否使用递归,主要看模型以后是否会扩展,以及哪种写法更容易维护。

在服务端集中执行授权

应用可以提供统一的授权入口:

authorize(userId, action, resourceId)

服务端负责将操作映射到所需级别,例如读取需要 Read,修改业务字段需要 Write,管理授权需要 Admin。删除和移动对象是否要求 Admin,应由业务规则单独确定。

所有读写路径都应执行同一套授权规则,包括批量操作、导出和后台任务。前端隐藏按钮可以改善体验,但服务端仍须检查权限。

创建对象时,新对象尚不存在,应检查父对象上的创建权限。移动 Department 或 Position 时,需要同时检查来源与目标范围的权限,并明确移动后按新的祖先链重新计算权限。

列表查询应在查询中加入授权过滤,或者先批量求出可访问的对象集合,再执行分页。取出全部对象后在应用内逐条检查,容易导致分页数量不准,也会产生大量重复查询。

如果要求严格撤权,还要处理“检查通过后、写入前权限被撤销”的并发情况。仅将两条语句放进事务未必足够,需要根据数据库隔离级别,配合锁定或其他并发控制方案。

用实际查询判断是否需要优化

判断查询是否过慢,不能只看 JOIN 的数量。应结合授权表规模、索引命中情况、列表访问方式和执行计划来分析。

初始方案可以使用邻接表,也就是 parent_id,再根据测量结果选择优化方式:

方案适用情况需要承担的成本
邻接表与祖先查询层级浅,希望写入和移动简单查询时查找祖先
闭包表 ancestor_id, descendant_id经常查询祖先、后代集合移动子树时更新关系
有效权限缓存或派生表授权读取频繁,实时计算已成为瓶颈失效、重建与撤权一致性

闭包表保存祖先与后代关系,也可以包含对象到自身的记录。它能减少查询时的层级遍历,同时需要承担关系维护的成本。

缓存有效权限时,祖先授权变更和对象移动都可能影响整棵子树。需要提前确定撤权允许延迟多久,并保证能够从权威数据重建缓存。

文档数据库也可以用父引用或祖先 ID 路径建模。选择前,应确认它的事务能力和查询方式能否满足对象移动、授权更新与撤权的一致性要求。

OAuth scopes、LDAP 与独立授权服务的边界

OAuth scopes 可以与对象授权配合使用。RFC 6749 将 scope 的具体含义交给授权服务器定义,没有规定 Company、Department、Position 的继承算法,参见 RFC 6749 第 3.3 节。

可以让 scope 限制令牌可执行的 API 操作,由对象授权判断当前用户能否操作具体对象。例如,即使拥有 positions:write scope,仍需检查用户对目标 Position 是否拥有 Write。对象关系和授权会变化,长期有效的权限快照会增加撤权难度,因此无需把所有对象授权都放进令牌。

LDAP 可以提供用户、组和已有的目录关系。如果业务对象的生命周期由应用数据库管理,仅为实现这三级继承而在 LDAP 中再维护一份对象关系,会增加同步和故障处理工作。已有企业目录时,可以用其中的用户或组标识作为授权主体,业务对象关系仍由业务系统维护。

当多个服务需要共享授权模型,或者关系扩展到用户组、项目成员、共享对象和委托关系时,可以评估独立的 ReBAC(Relationship-Based Access Control)服务。OpenFGA 的父子对象建模文档介绍了如何通过父对象关系推导子对象权限。

采用独立服务时,需要明确每类数据由谁负责,才能维持 single source of truth。例如,业务数据库负责对象关系和显式授权,授权服务保存可重建的查询投影。也可以由授权服务负责显式授权,但需要相应设计对象创建、删除和关系变更的协调流程。OpenFGA 的授权数据权威来源说明讨论了这些取舍。

当前三层模型用同一个数据库保存对象关系与显式授权,并提供统一的服务端授权入口,就能覆盖核心需求。是否拆出授权服务,再根据共享需求、关系复杂度和实际性能决定。

备注:内容仅供参考。