news 2026/9/17 4:42:27

NocoBase 插件开发(服务端)——ACL 权限控制完整指南:Snippet、allow/deny、固定参数与权限中间件

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
NocoBase 插件开发(服务端)——ACL 权限控制完整指南:Snippet、allow/deny、固定参数与权限中间件

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)机制,用于控制数据源上每个资源操作(如listcreatedestroy)的访问权限。本篇指南以 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 的实现可以看到三个关键细节:

  1. .*后缀自动剥离:注册时snippet.name = snippet.name.replace('.*', ''),因此pm.users.*会以pm.users为名注册(测试用例见 snippet.test.ts)。
  2. 非法命名直接抛错:若名称仍包含*或以.结尾,会抛出Invalid snippet name错误。
  3. 同名片段自动合并:同一名称重复注册时,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()会把资源:操作与条件记录到AllowManagerAllowManager内部(allow-manager.ts)预置了三个具名条件:

条件名判断逻辑
public恒为true,任何请求都放行
loggedInctx.state.currentUser存在(已登录)
allowConfigure当前角色存在且其策略开启了allowConfigure

其中loggedInpublicallowConfigure都通过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默认还携带三个字段:canctx.can的权限判断结果)、resourceNameactionName;在核心中间件运行后,还会追加deferred(针对firstOrCreateupdateOrCreate等需要先执行再判断的操作)、parsedParamsrawParamsmergedParams等字段。中间件通过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)接收资源名与操作名)。

多个固定参数会叠加合并,合并策略为:filterandMerge(多个 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别名
createAdd newnew-datacreate
viewViewold-dataget,list,query
updateEditold-dataupdate,move
destroyDeleteold-datadestroy

viewcreateupdate均开启了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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 4:39:55

SpringBoot+微信小程序物业管理系统:从报修工单到智慧社区

1. 选题拆解与整体技术方案1.1 医院家属小区和普通小区到底差在哪我接手这个选题的时候&#xff0c;第一反应是“这不就是个物业管理系统吗”。真去现场看过才明白&#xff0c;医院家属小区和外面的普通商业小区&#xff0c;在物业管理上完全是两套逻辑。首先&#xff0c;医院家…

作者头像 李华
网站建设 2026/9/17 4:39:05

埋点平台选型实战指南:神策、PostHog、ClkLog与开源栈深度对比

1. 埋点平台选型这件事&#xff0c;真不是挑个“能用的工具”那么简单2026年再谈埋点平台选型&#xff0c;已经完全不是五年前那种“找个SDK接入、看下漏斗报表”的轻量级决策了。神策、PostHog、ClkLog 这三个名字在数据团队晨会里出现的频率&#xff0c;已经和“用户分群策略…

作者头像 李华
网站建设 2026/9/17 4:38:10

E2E测试异常场景拆解:与单测、集成测试的边界与Vue落地实践

作为写测试用例写到想吐&#xff0c;但又不得不承认它救过我好几次命的人&#xff0c;今天想把E2E测试&#xff08;端到端测试&#xff09;这个话题彻底聊透。尤其是"异常场景怎么测"和"它跟单元测试、集成测试到底差在哪"这两件事。很多团队把单测跑绿了就…

作者头像 李华
网站建设 2026/9/17 4:34:44

Maven从安装到配置实战:环境变量与阿里云镜像那些坑

1. 先搞明白&#xff1a;Maven到底在替我们干什么1.1 构建工具解决了什么现实问题如果你第一次接触Maven&#xff0c;可能已经在网上搜过“maven是干嘛的”这种问题。这句话问得没错&#xff0c;但大部分人得到的答案是“项目管理工具”“构建工具”&#xff0c;听完还是不知道…

作者头像 李华
网站建设 2026/9/17 4:33:23

共享文件打不开?从SMB、445端口到NTFS权限分层排查

1. 先别急着改设置&#xff1a;搞清共享文件打不开到底卡在哪一步共享文件打不开这件事&#xff0c;几乎每个帮人修过电脑的人都遇到过。我当时的第一反应跟大多数人一样——关防火墙、重装系统、重启路由器&#xff0c;三板斧抡完还是那句"Windows 无法访问 \192.168.1.1…

作者头像 李华