首页
/ Strapi @strapi/permissions 权限引擎深度解析:基于 CASL 的参数化动作、条件求值与钩子体系

Strapi @strapi/permissions 权限引擎深度解析:基于 CASL 的参数化动作、条件求值与钩子体系

2026-09-04 11:53:25作者:温玫谨Lighthearted

本文以 Strapi 仓库中的 权限引擎介绍文档 为核心,系统讲解 @strapi/permissions 包的架构设计与实际用法。@strapi/permissions 是 Strapi 的权限管理层,构建在 CASL ability 系统之上,提供参数化动作(parametrized actions)、条件求值(conditional evaluation)与钩子系统(hook system)等能力,是 RBAC、用户与权限(users-permissions)、API Token 等上层功能的基础设施。读完后你将能够:理解权限引擎的 Domain 数据模型与 Engine 执行管线;掌握条件注册、generateAbility 生成 Ability、ability.can 求值的完整流程;并看懂条件如何被编译为 sift 查询、钩子如何拦截或改写权限规则。

一、包定位与核心能力

原文档给出的定位是:@strapi/permissions 是一套精细化的权限管理系统,构建于 CASL 的 ability 系统之上,扩展出参数化动作、条件求值与钩子机制三大高级特性,作为 Strapi 构建自定义权限体系(RBAC、users-permissions、API Token 等)的骨架。

packages/core/permissions/package.json 可以看到,该包当前的核心依赖印证了这一设计:

  • @casl/ability(6.7.5):ability 的构建与 can 求值底座;
  • sift(16.0.1):内存中匹配条件查询对象(RBAC condition);
  • qs(6.15.3):将动作参数序列化为 action?param=value 形式;
  • @strapi/utils:提供 providerFactoryhooks 等基础设施。

原文档列出的四个关键特性,与源码实现一一对应:

特性 文档描述 源码落点
动态求值 在请求时实时校验,保证权限与最新上下文一致 generateAbility 按当前上下文重新构建 Ability
参数化动作 动作可携带上下文参数,如 publish?postId=123 engine/index.ts 中用 qs.stringify 拼接动作串
条件逻辑 依据资源状态或用户数据做细粒度判定 条件 handler 并发求值,结果编译为 $and/$or 查询
钩子系统 在权限校验各阶段注入自定义行为 5 个命名钩子,定义于 engine/hooks.ts

包的公共 API 极简:src/index.ts 仅导出 domainengine 两个命名空间,文档中 import { engine, domain } from '@strapi/permissions' 即源于此。

二、核心架构:Engine 与 Domain 两个视角

2.1 Engine 视角:提供者、引擎与钩子

原文档给出了一张 Engine 架构图(mermaid),其表达的结构为:Action Provider 与 Condition Provider 两个提供者注入 Permission Engine;Engine 挂载 Hook System 并驱动 Ability Generator;Ability Generator 落到 CASL Integration;生命周期钩子最终作用于权限校验(Validation)、格式化(Formatting)与求值(Evaluation)三个环节

对照 engine/index.tsnewEngine 实现,这张图与实际执行结构完全吻合:

export interface EngineParams {
  providers: { action: ActionProvider; condition: ConditionProvider };
  abilityBuilderFactory?(): abilities.CustomAbilityBuilder;
}
  • 创建引擎必须传入 providers(action 与 condition 两个 provider,通常由 @strapi/utilsproviderFactory() 创建);
  • abilityBuilderFactory 可选,默认使用内置的 CASL builder(见 engine/abilities/casl-ability.ts),传入自定义 factory 即可替换整个 ability 类型——这正是文档架构图里 "CASL Integration" 可插拔的含义。

Engine 实例对外暴露三个成员(见同文件中的 Engine 接口):

成员 作用
hooks 只读访问引擎的钩子集合
on(hook, handler) 注册钩子处理函数;注册非法钩子名会直接抛出错误并列出所有合法钩子
generateAbility(permissions, options?) 核心入口:把权限数组编译为一个 CASL Ability 并返回

2.2 Domain 视角:Permission 的数据结构

原文档的 Domain 图描述为:Domain 命名空间下是 Permission,Permission 又关联 Action、Subject、Conditions、Properties、ActionParameters 五个要素。源码中这一结构由 domain/permission/index.ts 定义:

export interface Permission {
  action: string;
  actionParameters?: Record<string, unknown>;
  subject?: string | object | null;
  properties?: Record<string, any>;
  conditions?: string[];
}

该模块还导出了四个工具函数:

  • create(attributes)pick 出合法字段(actionsubjectpropertiesconditions)后与默认值 { conditions: [], properties: {}, subject: null } 合并,保证产出的 Permission 结构完整;
  • addCondition(condition, permission):向 Permission 追加一个条件名(自动去重),供 before-evaluate 钩子在求值前动态加条件;
  • sanitizePermissionFields(permission):仅保留合法字段,过滤脏数据;
  • getProperty(key, permission):从 properties 中安全取值。

另一个值得注意的类型是 types.ts 中的 PermissionRule——它是"求值完成后、注册进 Ability 之前"的规则形态,action 可以是字符串或 ParametrizedAction{ name, params }),并且比 Permission 多了一个 condition?: Record<string, unknown> 字段,用于携带条件求值产出的查询对象:

export interface ParametrizedAction {
  name: string;
  params: Record<string, unknown>;
}
export interface PermissionRule {
  action: string | ParametrizedAction;
  subject?: Subject | null;
  properties?: { fields?: string[] };
  condition?: Record<string, unknown>;
}

从源码结构看,Permission 是"输入",PermissionRule 是"输出",Engine 的职责就是完成这一转换。

三、权限求值管线:一条 Permission 如何变成 Ability 规则

这是原文档"Hook System / Conditional Logic"特性的核心,也是理解整个包的关键。generateAbility(permissions, options) 的循环逻辑(见 engine/index.ts)为:

async generateAbility(permissions, options: Record<string, unknown> = {}) {
  const { can, build } = abilityBuilderFactory();

  for (const permission of permissions) {
    const register = this.createRegisterFunction(can, options);
    await evaluate({ permission, options, register });
  }

  return build();
}

即:对每条 Permission 走一遍内部 evaluate 管线,管线末端调用 register 把最终规则写入 ability builder;全部处理完再 build() 出 Ability。evaluate 内部的完整链路为:

  1. before-format::validate.permission(bail 钩子):处理函数返回 false 时该权限直接作废,后续步骤全部跳过——可用于"白名单/黑名单"式拦截。
  2. format.permission(waterfall 钩子):以当前 Permission 为输入串联多个处理函数,允许改写权限结构。
  3. after-format::validate.permission(bail 钩子):格式化后再做一次校验,返回 false 同样作废。
  4. before-evaluate.permission(series 钩子):拿到一个带 addCondition 能力的上下文,可以在求值前给权限追加条件(hooks.tscreateBeforeEvaluateContext 实现了这一 API)。
  5. 条件解析与并发求值
    • providers.condition.get(id) 按名称解析每个条件,解析不到或 handler 不是函数的条件会被静默剔除;
    • 每个条件 handler 收到的入参是 options 与一份深拷贝的 permission 合并后的上下文,因此 handler 既知道请求上下文(如当前用户),也知道这条权限本身;
    • 求值结果只保留布尔值与对象两类:全部为 false 时该权限不注册;存在 true 或结果为空时按无限制注册;否则把返回的对象结果打包为 condition: { $and: [{ $or: results }] }——即"任一条件对象命中即放行"的语义。
  6. 参数化动作拼接:若 Permission 带 actionParameters,动作名会被拼成 actionName?${qs.stringify(actionParameters)} 形式(engine/index.ts),对应文档中的 publish?postId=123 示例。
  7. before-register.permission(series 钩子):在真正调用 can 注册前触发;其上下文(hooks.tscreateWillRegisterContext)提供 condition.and(...)condition.or(...) 两个 API,允许钩子代码在注册前向规则追加 $and/$or 查询条件。

五个钩子的完整清单与类型定义在 engine/hooks.ts,均基于 @strapi/utils 的 async hook 原语创建(bail / waterfall / series 三种)。engine.on 注册非法钩子名时会抛出带合法钩子列表的错误信息,便于排查。

四、CASL Ability 构建:sift 条件匹配与参数化动作的 can

引擎产出 PermissionRule 之后,最终的 can(action, subject, fields, condition) 注册与 ability.can(...) 求值都发生在 engine/abilities/casl-ability.tscaslAbilityBuilder 中。这里有三个值得展开的细节:

1)字段级授权can(permission) 会把 properties.fields 原样传给 CASL 的 can

can(caslAction, isNil(subject) ? 'all' : subject, fields, isObject(condition) ? condition : undefined)

这就是文档示例中 conditions: ['isOwner'], properties: { fields: ['title', 'content'] } 的落点——不仅授权 update 操作,还限定了只能更新 titlecontent 两个字段。

2)sift 白名单操作符。Ability 构建时传入 conditionsMatcher,用 sift 在内存中匹配条件对象,且只开放白名单内的操作符(casl-ability.ts):$or$and$eq$ne$in$nin$lt$lte$gt$gte$exists$elemMatch。使用白名单之外的操作符会抛出明确错误:RBAC condition uses unsupported operator "..."单元测试 中专门用 $startsWith 覆盖了这条报错路径。

3)参数化动作的求值装饰build() 返回的 Ability 会被装饰:ability.can 被包装,第一个参数若是 { name, params } 形态的 ParametrizedAction,会自动转换为 name?queryString 再交给原生 can。因此校验侧既能写 ability.can('read', 'article'),也能直接传参数化动作对象,与注册侧的字符串化动作精确匹配。

五、集成示例:从 Provider 到 ability.can 的完整流程

原文档给出了一个完整集成示例,这里在保留其六步结构的基础上,结合 packages/core/permissions/README.md 与测试用例的用法补充说明:

import { engine, domain } from '@strapi/permissions';

// 1. 定义 Providers(action / condition 各一个)
const providers = {
  action: providerFactory(),
  condition: providerFactory(),
};

// 2. 注册自定义条件:handler 入参为 { ...options, permission } 上下文
providers.condition.register({
  name: 'isOwner',
  handler: (ctx) => ctx.user.id === ctx.resource.ownerId,
});

// 3. 创建引擎
const permissionEngine = engine.new({ providers });

// 4. 定义权限(domain.permission.create 会补全 conditions/properties/subject 默认值)
const permissions = [
  domain.permission.create({
    action: 'read',
    subject: 'article',
    conditions: ['isPublished'],
  }),
  domain.permission.create({
    action: 'update',
    subject: 'article',
    conditions: ['isOwner'],
    properties: {
      fields: ['title', 'content'],
    },
  }),
];

// 5. 生成 Ability(options 会透传给每个条件 handler)
const ability = await permissionEngine.generateAbility(permissions);

// 6. 求值
const canReadArticle = ability.can('read', 'article');

补充几点实操细节:

  • 安装yarn add @strapi/permissions(见包内 README);
  • provider 的作用:condition provider 本质是一个按名称取 handler 的注册表,evaluate 管线第 5 步的 providers.condition.get(id) 依赖它的 get 方法。测试代码 permissions.engine.vitest.test.ts 展示了如何为 condition provider 挂上 get,以及条件 handler 返回 true / false / 查询对象(如 { id: 125 })时的不同注册结果;
  • 钩子拦截示例(来自包内 README 的 JS 用法):
const engine = permissions.engine
  .new({ providers })
  .on('before-format::validate.permission', ({ permission }) => {
    if (permission.action === 'read') {
      return false; // 直接作废该权限
    }
  });

六、在 Strapi 中的真实消费场景

@strapi/permissions 并非孤立存在,它是 Strapi 管理端权限判定的底层引擎。从源码结构看,packages/core/admin/server/src/services/permission/engine.ts 中管理端 permission 服务基于该包构建引擎;内容 API Token(strategies/content-api-token.ts)与数据迁移(strategies/data-transfer.ts)等鉴权策略同样依赖其 generateAbility 能力。换言之,原文档所说的"enabling developers to design customized permission systems tailored to ... RBAC, users and permissions, or API tokens",在仓库中对应着 admin 服务的 RBAC 体系与若干内置鉴权策略:业务层把"角色权限集合"翻译成 Permission 数组,调用引擎生成 Ability,再在请求时用 ability.can(action, subject, fields, fieldValues) 做字段级、条件级的最终裁决。

七、小结

  • 两个命名空间domain.permission 负责权限结构的创建与清洗(create / addCondition / sanitizePermissionFields / getProperty);engine 负责把权限数组编译为 CASL Ability;
  • 一条管线validate → format → validate → before-evaluate → 条件并发求值 → 注册前钩子 → can(...),五处钩子均可按引擎实例定制;
  • 两个可插拔点providers 决定动作与条件的来源,abilityBuilderFactory 决定 ability 的类型;
  • 边界清晰:条件对象只支持 sift 白名单操作符,不支持的操作符会得到明确报错而非静默失败,配合 单元测试 中的钩子拦截、条件真/假/对象结果等用例,可以完整覆盖权限引擎的可验证行为边界。

理解以上内容后,即可在自己的 Strapi 扩展中直接复用该引擎:注册条件 provider、挂接生命周期钩子、用 generateAbility 生成 Ability,并在请求链路中调用 ability.can 完成字段级、条件级的动态授权。

登录后查看全文
热门项目推荐
相关项目推荐