
Strapi strapi/permissions 权限引擎实战从 CASL Ability 构建到 Hook 拦截机制【免费下载链接】strapi Strapi is the leading open-source headless CMS. It’s 100% JavaScript/TypeScript, fully customizable, and developer-first.项目地址: https://gitcode.com/GitHub_Trending/st/strapistrapi/permissions是 Strapi 核心权限引擎包位于 packages/core/permissions负责把一组「权限规则」编译成 CASLcasl/ability的 Ability 对象供运行时判断「某主体能否执行某动作、能访问哪些字段、是否满足查询条件」。本篇将基于该包的官方文档与 源码实现完整讲清引擎的创建、providers 依赖、能力生成流程、五类 Hook 的执行时序与 Strapi Admin 后端对它的真实用法帮助你既能独立使用该包也能理解 Strapi RBAC 底层的判定原理。安装与包定位文档给出的安装方式yarn add strapi/permissions从 package.json 可以看到该包当前版本为 5.52.2其运行时依赖非常精简casl/ability6.7.5 —— 能力模型的底层实现Ability / AbilityBuilderstrapi/utils—— 提供providerFactoryproviders 的容器与hooksHook 系统lodash—— 引擎内部的函数式处理qs—— 用于序列化「带参数动作」parametrized actionsift—— 在内存中对 RBAC 条件查询condition做匹配。包的公开入口见 src/index.ts仅导出两个命名空间import * as domain from ./domain; // 权限领域对象create / addCondition / getProperty import * as engine from ./engine; // 权限引擎new / abilities export { domain, engine };因此文档示例中的permissions.engine.new(...)、permissions.domain等调用都来自这里。创建引擎实例providers 是硬性依赖创建引擎的核心 API 是engine.new(params)其中params的类型定义在 src/engine/index.tsexport interface EngineParams { providers: { action: ActionProvider; condition: ConditionProvider }; abilityBuilderFactory?(): abilities.CustomAbilityBuilder; }两个硬性要求必须同时提供action和condition两个 provider。文档明确指出“You need to give both an action and a condition provider as parameters when instantiating a new permission engine instance. They must be contained in aprovidersobject property.” 从源码看conditionprovider 会在条件求值阶段被providers.condition.get(id)逐个解析src/engine/index.ts如果找不到对应条件或其 handler 不是函数该条件会被静默过滤。可选传入abilityBuilderFactory定制generateAbility返回的 Ability 类型默认使用abilities.caslAbilityBuilder即casl/ability的 builder对应文档中的说明“By default itll use acasl/abilitybuilder.”一个最小可用的完整示例文档示例 测试文件中 providers 的真实构造方式参考 单元测试const permissions require(strapi/permissions); const { providerFactory } require(strapi/utils); // providersaction / condition 两个提供者容器 const providers { action: providerFactory(), condition: providerFactory(), }; // 注册一个条件真实 Strapi 中由插件注册如 plugin::content-manager.isOwner await providers.condition.register(isAuthor, { name: isAuthor, async handler() { return true; }, }); const engine permissions.engine.new({ providers }); const ability await engine.generateAbility([ { action: read }, { action: delete, subject: foo }, { action: update, subject: bar, properties: { fields: [foobar] } }, { action: create, subject: foo, properties: { fields: [foobar] }, conditions: [isAuthor], }, ]); ability.can(read); // true ability.can(publish); // false ability.can(update, foo); // false ability.can(update, bar); // true这里体现的四条判定规律值得注意{ action: read }不指定 subject会被注册为对all主体生效——CASL builder 内部对subject为空的规则做了归一化处理见下文「CASL Builder 细节」publish动作未注册判定为falseupdate foo未注册update bar已注册create foo附带isAuthor条件条件通过则以无条件形式注册若条件 handler 返回对象查询片段则会被合并为 condition 挂在规则上。条件conditions求值逻辑源码级深挖generateAbility内部对每条权限执行evaluate流程src/engine/index.ts其中条件求值是最复杂的分支源码逻辑可以归纳为四步解析resolveConditions用providers.condition.get(id)把conditions: [isAuthor]之类的字符串 ID 解析为{ name, handler }对象并过滤掉不存在或 handler 非函数的无效条件执行每个条件的 handler 以_.merge(options, { permission: cloneDeep(permission) })为入参并行执行Promise.all即 handler 可以拿到generateAbility(permissions, options)传入的第二个参数在 Admin 中通常是当前用户对象以及被克隆的权限对象本身过滤结果只保留返回boolean或object的结果三种分支注册src/engine/index.tsif (evaluatedConditions.every(resultPropEq(false))) { return; // 所有条件都返回 false → 该权限整体作废不注册 } if (_.isEmpty(evaluatedConditions) || evaluatedConditions.some(resultPropEq(true))) { return register({ action, subject, properties }); // 有任一条件返回 true → 无条件注册 } // 全部返回对象查询片段→ 合并为 $and/$or 条件后注册 return register({ action, subject, properties, condition: { $and: [{ $or: results }] } });也就是说条件之间是「或」语义——任一条件为真则权限生效全部为假则权限被丢弃全部返回查询对象时这些对象会被包进{ $and: [{ $or: [...] }] }作为 Ability 规则的 condition最终在ability.can()调用时由内存查询匹配器判定。单元测试中的hasId125/hasId200两个返回{ id: ... }对象的条件正是覆盖此分支见 测试用例。此外还有一个容易忽略的细节如果权限携带actionParameters引擎会用qs.stringify将其拼进动作名src/engine/index.tsif (actionParameters Object.keys(actionParameters).length 0) { action ${actionName}?${qs.stringify(actionParameters)}; }五类 Hook完整时序与各自能力文档说“You can also register to some hooks for each engine instance. Seelib/engine/hooks.js-createEngineHooksfor available hooks.”lib/为旧版目录名当前源码位于 src/engine/hooks.ts。当前版本共有5 个 Hook且各自对应不同的钩子类型这直接决定了 handler 的写法能否返回false中断、能否返回新对象覆盖Hook 名称钩子类型strapi/utils hooks可做什么before-format::validate.permissionAsyncBailHook返回false立即废弃该权限上下文提供只读permission克隆format.permissionAsyncSeriesWaterfallHook返回新对象可改写权限前一个 handler 的输出作为下一个的输入after-format::validate.permissionAsyncBailHook格式化后再校验返回false废弃before-evaluate.permissionAsyncSeriesHook上下文额外提供addCondition(condition)方法可向权限动态追加条件before-register.permissionAsyncSeriesHook注册前最后拦截上下文提供condition.and(obj)/condition.or(obj)向规则追加查询条件其中前三个的执行顺序写死在evaluate函数中src/engine/index.tsbefore-format::validate.permission →false 则终止 format.permission →waterfall可改写权限对象 after-format::validate.permission →false 则终止 before-evaluate.permission而before-register.permission在createRegisterFunction包装的 register 阶段触发src/engine/index.tsbefore-evaluate.permission在条件解析前触发。文档示例一用 bail hook 拦截权限const engine permissions.engine .new({ providers }) .on(before-format::validate.permission, ({ permission }) { if (permission.action read) { return false; // bail终止该权限的后续流程 } }); const ability await engine.generateAbility([ { action: read }, // ...其余同前 ]); ability.can(read); // false因为校验 hook 阻止了引擎注册该权限这里{ permission }就是createBeforeEvaluateContext/createValidateContext生成的上下文——注意上下文的permission是克隆cloneDeephandler 里对它的普通修改不会影响原权限对象只有 bail hook 返回false或 waterfall hook 返回新对象才能产生实际影响。文档示例二用 waterfall hook 改写动作名const engine permissions.engine .new({ providers }) .on(before-format::validate.permission, ({ permission }) { if (permission.action modify) return false; // 拦截最终动作名 }) .on(after-format::validate.permission, ({ permission }) { if (permission.action update) return false; }) .on(format.permission, ({ permission }) { if (permission.action update) { return { ...permission, action: modify }; } if (permission.action delete) { return { ...permission, action: remove }; } return permission; }); const ability await engine.generateAbility([{ action: update }, { action: delete }]); ability.can(update); // false ability.can(modify); // true因为 format.permission 把它改成了 modify ability.can(delete); // false被改写成了 remove ability.can(remove); // true这个例子精确演示了执行时序before-format::validate看到改写之前的动作名所以拦截modify不影响由delete改出的remove而after-format::validate看到改写之后的动作名。文档中的注释 “before-format::validate.permission validates before format.permission changed it” 说的正是这一点。注册前追加条件before-register 的上下文能力createWillRegisterContextsrc/engine/hooks.ts在before-register.permission阶段提供了比文档示例更进一步的能力——直接向即将注册的规则追加查询条件engine.on(before-register.permission, (ctx) { // 给该权限追加 $and 条件只有满足查询的主体才放行 ctx.condition.and({ role: { $in: [admin, editor] } }); // 或追加 $or 条件 ctx.condition.or({ id: { $eq: 1 } }); });CASL Builder 细节subject 归一化、字段限制与条件匹配器默认 builder 实现位于 src/engine/abilities/casl-ability.ts有三个关键设计subject 为null/undefined时注册为allproperties.fields直接作为 CASL 的字段参数传入can(action, subject, fields, condition)——这就是为什么{ action: read }能对任意主体生效而{ action: update, subject: bar, properties: { fields: [foobar] } }只允许更新foobar字段参数化动作parametrized actionPermissionRule.action允许{ name, params }形式类型定义见 src/types.tsbuilder 会将其序列化为actionName?keyvalue字符串并且build()后返回的 Ability 的can方法被装饰decorate调用ability.can({ name, params }, subject)时同样会自动序列化保证注册与查询两端格式一致内存条件匹配器build({ conditionsMatcher })中用sift创建查询测试器且只开放了一组白名单操作符$or、$and、$eq、$ne、$in、$nin、$lt、$lte、$gt、$gte、$exists、$elemMatch。若条件里使用了白名单之外的操作符如$startsWith会抛出明确的错误RBAC condition uses unsupported operator ...。单元测试中unsupportedOperator条件正是覆盖此边界。CustomAbilityBuilder接口casl-ability.ts要求can、buildParametrizedAction、build三个成员这就是abilityBuilderFactory定制点需要满足的契约。权限领域对象domainpermissions.domain暴露了权限的构造与操作函数src/domain/permission/index.tsPermission接口字段action必填、subject、properties、conditions、actionParameterscreate(attributes)用_.pick只保留这四个字段并合并默认值conditions: []、properties: {}、subject: null等价于权限入参的「消毒」addCondition(condition, permission)向conditions数组去重追加条件before-evaluate.permission上下文的addCondition方法内部调用的就是它src/engine/hooks.tsgetProperty(property, permission)从properties中取嵌套值如getProperty(fields, permission)。真实集成Strapi Admin 后端如何驱动这个引擎strapi/permissions并非孤立存在——Strapi 管理面板的 RBAC 直接构建在它之上典型集成代码见 packages/core/admin/server/src/services/permission/engine.ts它恰好示范了文档中各 Hook 的典型用途const engineInstance engine .new({ providers }) // 1. 校验动作是否在 action 注册表中存在不存在则拦截 .on(before-format::validate.permission, ({ permission }) { const action providers.action.get(permission.action); if (!action) { strapi.log.debug(Unknown action ${permission.action} ...); return false; } }) // 2. 按 action.applyToProperties 清掉不允许的 properties .on(format.permission, (permission) { /* ... */ }) // 3. fields 为空数组不授权任何字段时整条权限作废 .on(after-format::validate.permission, ({ permission }) { const { fields } permission.properties; if (isArray(fields) isEmpty(fields)) { return false; } });该服务最终对外提供三个方法generateUserAbility(user)查出用户的角色权限集后调用generateAbility(permissions, user)把用户对象作为 options 传给条件 handler、generateTokenAbility(tokenPermissions, owner)管理端 Token 场景和checkMany(ability, permissions)批量ability.can判断。这也解释了文档示例中generateAbility(permissions)之外实际还有一个options参数的用途——Admin 场景下它就是当前用户条件 handler 可以基于它判断「是否作者」「是否所有者」等。小结什么时候用引擎什么时候只用 Ability结合文档与源码这个包的使用可以归纳为两层权限定义/生成层使用engine.newgenerateAbility把结构化的权限规则含条件、字段编译成 Ability适合插件/后端在启动或鉴权前构建能力集配合 providers 管理动作与条件运行时判定层只使用生成的ability.can(action, subject, fields)在路由控制器或策略中做细粒度判断支持字段级properties.fields与条件查询级的授权。需要注意的适用前提该包要求 Node.js20.0.0见 package.json 的engines条件匹配是在内存中通过sift白名单操作符完成的不能用于数据库层查询条件 handler 返回的查询对象会被包进$and/$or结构使用非白名单操作符会直接抛错。完整行为边界可以参考 引擎单元测试 与 权限领域测试。【免费下载链接】strapi Strapi is the leading open-source headless CMS. It’s 100% JavaScript/TypeScript, fully customizable, and developer-first.项目地址: https://gitcode.com/GitHub_Trending/st/strapi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考