NocoBase 插件开发(服务端)——ACL 权限控制完整指南:Snippet、allow/deny、固定参数与权限中间件
【免费下载链接】nocobaseNocoBase is an open-source AI + no-code platform for building business systems fast. Instead of generating everything from scratch, AI works on top of production-proven infrastructure and a WYSIWYG no-code interface, so you get both speed and reliability.项目地址: https://gitcode.com/GitHub_Trending/no/nocobase
NocoBase 的服务端通过@nocobase/acl提供了一套基于资源操作(Action)的访问控制列表(ACL)机制,用于控制数据源上每个资源操作(如list、create、destroy)的访问权限。本篇指南以 NocoBase 插件开发文档《ACL 权限控制(服务端)》为主体,结合packages/core/acl源码与测试用例,系统讲解权限片段(Snippet)、跳过角色约束(allow)、权限中间件(use)、固定数据约束(addFixedParams)、权限判断(can)与可配置操作(setAvailableAction)六个核心能力,读完你将能在自己的插件中为自定义接口和数据资源配置细粒度的服务端权限。
ACL 对象的归属与访问方式
在动手注册权限之前,先明确 ACL 对象在 NocoBase 架构中的位置:
- ACL 对象归属于数据源(Data Source),通过
dataSource.acl访问。每个数据源拥有独立的 ACL 实例,互不干扰。 - 主数据源(Main Data Source)的 ACL 可以通过
app.acl快捷访问,这也是插件开发中最常用的入口。在 main-data-source.ts 中可以看到,MainDataSource在初始化时接收acl实例并挂载到自身,同时将acl.middleware()注册到资源管理器(resourceManager.use(this.acl.middleware(), { group: 'acl', after: 'auth' })),这意味着 ACL 检查位于鉴权(auth)之后、实际业务处理器之前。 - 其他数据源的 ACL 使用方式:通过
app.dataSourceManager获取对应数据源的acl属性,详细说明见 DataSourceManager 数据源管理。
ACL 的核心类ACL定义在 acl.ts,内部维护了角色表(roles)、可用策略(availableStrategy)、跳过规则(allowManager)、权限片段(snippetManager)、固定参数(fixedParamsManager)以及按拓扑序排列的权限中间件(middlewares)。
注册权限片段(Snippet)
权限片段(Snippet)可以把一组常用的权限组合注册为可复用的权限单元,角色绑定 Snippet 后即可获得对应的一组权限,减少重复配置。
acl.registerSnippet({ name: 'ui.customRequests', // ui.* 前缀表示允许在界面上配置的权限 actions: ['customRequests:*'], // 对应资源操作,支持通配符 });底层实现与命名约束
从 snippet-manager.ts 的实现可以看到三个关键细节:
.*后缀自动剥离:注册时snippet.name = snippet.name.replace('.*', ''),因此pm.users.*会以pm.users为名注册(测试用例见 snippet.test.ts)。- 非法命名直接抛错:若名称仍包含
*或以.结尾,会抛出Invalid snippet name错误。 - 同名片段自动合并:同一名称重复注册时,
actions会做并集去重(new Set([...existed.actions, ...snippet.actions])),因此多个插件可以安全地向同一个片段追加操作,测试见 snippet.test.ts。
通配符与否定片段
角色在绑定片段时(role.snippets.add(...))可以使用minimatch通配符,也可以使用!前缀做否定排除:
adminRole.snippets.add('sc.*'):绑定所有sc.开头的片段;adminRole.snippets.add('!sc.collection-manager.gi'):显式排除某个片段。
effectiveSnippets()会先计算「允许集合减去被排除集合」得到生效片段,再逐条用minimatch匹配资源:操作路径(见 acl-role.ts)。注意否定规则优先于允许规则——即使某个动作同时被允许片段和否定片段命中,最终也是被拒绝。这一行为有完整测试覆盖(snippet.test.ts)。
跳过角色约束的权限(allow)
acl.allow()用于让某些操作绕过角色约束,适用于公开 API、需要动态判断权限的场景,或者需要基于请求上下文做权限判断的情况。
// 公开访问,无需登录 acl.allow('app', 'getLang', 'public'); // 已登录用户即可访问 acl.allow('app', 'getInfo', 'loggedIn'); // 基于自定义条件判断 acl.allow('orders', ['create', 'update'], (ctx) => { return ctx.auth.user?.isAdmin ?? false; });condition 参数说明
'public':任何用户(包括未登录用户)都可访问,无需任何身份验证;'loggedIn':仅已登录用户可访问,需要有效的用户身份;(ctx) => Promise<boolean>或(ctx) => boolean:自定义函数,根据请求上下文动态判断是否允许访问,可以实现复杂的权限逻辑。
底层实现:allow 是 skip 的别名
源码中allow()直接委托给skip()(见 acl.ts),skip()会把资源:操作与条件记录到AllowManager。AllowManager内部(allow-manager.ts)预置了三个具名条件:
| 条件名 | 判断逻辑 |
|---|---|
public | 恒为true,任何请求都放行 |
loggedIn | ctx.state.currentUser存在(已登录) |
allowConfigure | 当前角色存在且其策略开启了allowConfigure |
其中loggedIn、public、allowConfigure都通过registerAllowCondition(name, fn)注册为具名条件,自定义函数条件则直接在匹配时被调用(isAllowed中逐个 await 执行)。AllowManager还支持用*通配资源名或动作名,例如:
// 所有资源的 list 操作都对已登录用户开放 acl.allow('*', 'list', 'loggedIn');在请求到达时,AllowManager.aclMiddleware()会先于核心权限检查运行(在 acl.ts 中以tag: 'allow-manager', before: 'core'注册),一旦命中条件就将ctx.permission.skip置为true,从而跳过后续的 ACL 检查。相关测试见 allow.test.ts。
注册权限中间件(use)
acl.use()用于注册自定义权限中间件,可以在权限检查流程中插入自定义逻辑,通常和ctx.permission搭配使用,用于实现非常规权限控制。
典型应用场景:
- 公开表单场景:无用户无角色,但需要通过自定义密码来约束权限;
- 基于请求参数、IP 地址等条件的权限控制;
- 自定义权限规则,跳过或修改默认的权限检查流程。
通过ctx.permission控制权限:
acl.use(async (ctx, next) => { const { resourceName, actionName } = ctx.action; // 示例:公开表单需要验证密码后跳过权限检查 if (resourceName === 'publicForms' && actionName === 'submit') { const password = ctx.request.body?.password; if (password === 'your-secret-password') { // 验证通过,跳过权限检查 ctx.permission = { skip: true, }; } else { ctx.throw(403, 'Invalid password'); } } // 执行权限检查(继续 ACL 流程) await next(); });ctx.permission 属性说明
skip: true:跳过后续的 ACL 权限检查,直接允许访问;- 可以在中间件中根据自定义逻辑动态设置,实现灵活的权限控制。
从 acl.ts 的middleware()实现看,ctx.permission默认还携带三个字段:can(ctx.can的权限判断结果)、resourceName、actionName;在核心中间件运行后,还会追加deferred(针对firstOrCreate、updateOrCreate等需要先执行再判断的操作)、parsedParams、rawParams、mergedParams等字段。中间件通过Toposort排序执行,acl.use()注册的自定义中间件默认进入prep组,可用{ before, after, group }选项控制执行顺序。
为特定操作添加固定数据约束(addFixedParams)
addFixedParams可以为某些资源的操作添加固定的数据范围(filter)约束,这些约束会绕过角色限制直接生效,通常用于保护系统关键数据。
acl.addFixedParams('roles', 'destroy', () => { return { filter: { $and: [ { 'name.$ne': 'root' }, { 'name.$ne': 'admin' }, { 'name.$ne': 'member' }, ], }, }; }); // 即使用户拥有删除角色的权限,也无法删除 root、admin、member 这些系统角色底层实现:FixedParamsManager
FixedParamsManager(fixed-params-manager.ts)以资源:操作为键存储「固定参数生产函数」(Merger),并支持两类注册:
addFixedParams(resource, action, merger):为指定资源、指定操作注册固定参数;addGeneralFixedParams(merger):注册全局固定参数,对任意资源:操作生效(merger(resource, action)接收资源名与操作名)。
多个固定参数会叠加合并,合并策略为:filter用andMerge(多个 filter 条件以$and合并)、fields/whitelist/blacklist取交集、appends/except取并集、sort后写覆盖。这一点有测试佐证:fixed-params.test.ts 中连续注册两个'name.$ne'条件后,最终得到filter: { $and: [...] }的合并结果。
固定参数与角色权限叠加生效:在can()的getCanByRole中(acl.ts),固定参数通过assign(params, fixedParams)合并进最终结果,即便角色拥有destroy权限,也无法操作被固定参数排除的数据,确保系统内置角色、管理员账户等敏感数据不被误删或修改。
判断权限(can)
acl.can()用于判断某个角色是否有权限执行指定操作,返回权限结果对象或null,通常用在中间件或操作的 Handler 中,根据角色动态判断是否允许执行某些操作。
const result = acl.can({ roles: ['admin', 'manager'], // 可以传入单个角色或角色数组 resource: 'orders', action: 'delete', }); if (result) { console.log(`角色 ${result.role} 可以执行 ${result.action} 操作`); // result.params 包含了通过 addFixedParams 设置的固定参数 console.log('固定参数:', result.params); } else { console.log('无权限执行该操作'); }类型定义
interface CanArgs { role?: string; // 单个角色 roles?: string[]; // 多个角色(会依次检查,返回第一个有权限的角色) resource: string; // 资源名称 action: string; // 操作名称 } interface CanResult { role: string; // 有权限的角色 resource: string; // 资源名称 action: string; // 操作名称 params?: any; // 固定参数信息(如果通过 addFixedParams 设置了的话) }多个角色的行为:返回并集
如果传入多个角色,can()会依次检查每个角色并返回并集结果(acl.ts):第一个有权限的角色的结果作为基准,后续角色若有权限则把其params合并进基准结果。因此result.role记录的是首个命中的角色。
另外需要注意两个特殊行为:
root角色:当roles中包含root时,会直接缩短为['root'],且root角色在getCanByRole中直接返回权限结果而不做任何策略/片段检查——即 root 拥有全部权限;- 默认匿名角色:在请求中间件中,未登录请求的
roleName取自ctx.state.currentRole || 'anonymous'(acl.ts)。
请求上下文中的快捷方式
在请求处理中,ctx.can(...)是acl.can()的便捷封装(自动带上ctx.state.currentRoles或当前角色),ctx.permission.can即是对当前请求「资源:操作」的预计算结果,可以直接读取而无需重复调用。
注册可配置操作(setAvailableAction)
如果你希望自定义操作可以在界面上配置权限(比如在「角色管理」页面中显示),需要用setAvailableAction注册。注册后的操作会出现在权限配置界面中,管理员可以在界面上为不同角色配置操作权限。
acl.setAvailableAction('importXlsx', { displayName: '{{t("Import")}}', // 界面显示名称,支持国际化 type: 'new-data', // 操作类型 onNewRecord: true, // 是否在新记录创建时生效 });参数说明
- displayName:在权限配置界面显示的名称,支持国际化(使用
{{t("key")}}格式); - type:操作类型,决定该操作在权限配置中的分类:
'new-data':创建新数据的操作(如导入、新增等);'existing-data':修改已有数据的操作(如更新、删除等);
- onNewRecord:是否在新记录创建时生效,仅对
'new-data'类型有效; - aliases(可选):操作别名,
setAvailableAction会将别名写入actionAlias,在策略匹配、权限判断时自动解析到正式操作名(见 acl.ts); - allowConfigureFields(可选):是否允许在界面上配置该操作的字段范围。
内置可配置操作
服务端在创建 ACL 时(createACL)会批量注册一组内置操作,定义于 available-action.ts:
| 操作 | 显示名 | type | 别名 |
|---|---|---|---|
create | Add new | new-data | create |
view | View | old-data | get,list,query |
update | Edit | old-data | update,move |
destroy | Delete | old-data | destroy |
view、create、update均开启了allowConfigureFields: true,管理员可在界面为其配置允许操作的字段。注册后,该操作会出现在权限配置界面中,管理员可以在「角色管理」页面中配置该操作的权限。
插件中的综合应用示例
以仓库内置的plugin-acl插件为例(server.ts),可以看到各 API 的真实组合用法:
// 登录后即可访问的公开动作 this.app.acl.allow('users', 'setDefaultRole', 'loggedIn'); this.app.acl.allow('roles', 'check', 'loggedIn'); // root 角色全局放行 this.app.acl.allow('*', '*', (ctx) => { return ctx.state.currentRoles?.includes('root'); }); // 固定参数保护:系统角色不可删除 this.app.acl.addFixedParams('roles', 'destroy', () => { return { filter: { $and: [{ 'name.$ne': 'root' }, { 'name.$ne': 'admin' }, { 'name.$ne': 'member' }], }, }; }); // 保护内置数据范围(rolesResourcesScopes 的 all / own 不可删除、修改) this.app.acl.addFixedParams('rolesResourcesScopes', 'destroy', () => { return { filter: { $and: [{ 'key.$ne': 'all' }, { 'key.$ne': 'own' }], }, }; });这个示例同时印证了本文的多个要点:allow可用于放行登录用户的常规操作、root特判可用自定义条件实现、addFixedParams是保护内置数据最直接的手段。
相关链接
- ResourceManager 资源管理 — 注册自定义接口与资源操作
- Plugin 插件 — 在插件生命周期中注册权限
- Context 请求上下文 — 在请求中获取当前角色和权限信息
- Middleware 中间件 — ACL 中间件的注册与使用
- DataSourceManager 数据源管理 — 各数据源各自拥有独立的 ACL 实例
【免费下载链接】nocobaseNocoBase is an open-source AI + no-code platform for building business systems fast. Instead of generating everything from scratch, AI works on top of production-proven infrastructure and a WYSIWYG no-code interface, so you get both speed and reliability.项目地址: https://gitcode.com/GitHub_Trending/no/nocobase
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考