Vext 插件
如果你的项目已经使用 Vext、token 认证、Vext database 插件和 MonSQLize,可以用 permission-core/plugins/vext 把“认证后的用户”接到接口权限、数据权限和响应字段权限。普通接入时,业务代码最好仍然沿用 Vext 原来的 app.db.collection()、app.db.model() 和 service 写法;权限插件只负责在受保护请求里把这些入口安全增强。
先记住一句话:前端可以传 token,但 token 必须先由认证插件验证;permissionPlugin 只读取可信 req.auth。后面所有路由权限、数据权限和响应字段投影,都基于这个可信用户执行。
先看最终写法
接入完成后,插件配置通常长这样:
业务代码继续像 Vext 原来那样写:
你只需要先理解三点:
routes.protect:服务端配置哪些路由默认受保护,不需要每个路由都写permission: true。routes.public:服务端配置哪些路由明确公开,例如登录和健康检查。data.transparent: true:在受保护请求里,app.db.collection()/app.db.model()自动合入租户、数据行和字段权限;后台任务和公开路由仍然使用宿主原始 DB。
接入流程一览
如果暂时只做接口鉴权,只需要配置 routes.protect/public 和接口授权;如果 handler 或 service 要读写数据库,再开启 data.transparent;如果要裁剪返回字段,再看“响应字段投影”。完整选项和类型见 Vext 插件 API,本页只保留当前版本推荐接入路径。
前置条件
- Node.js
>=20.19.0,这是 Vext 0.3.26 的运行要求。 - 安装
permission-core、monsqlize@3.1.0和vextjs@0.3.26。 - 宿主已经有一个连接好的 MonSQLize 3.1 实例;如果使用 Vext database 插件,通常已经有
app.db和app.monsqlize。 - 认证插件先运行,验证 token,并写入可信
req.auth。
如果暂时只做路由权限,不需要先配置 data 或响应字段权限。只有要让 app.db.collection() / app.db.model() 在受保护请求里自动套数据权限时,才配置 data.transparent;只有要自动裁剪接口响应字段时,才提前用 menus.responses.set() 或 menus.config.save() 保存字段配置。响应字段的最小配置见本页“响应字段投影”。
1. 认证插件先验证 token
permission-core 不负责登录,也不会直接相信前端传来的 token。正确链路是:认证插件验证 token 签名、会话和过期时间,然后把可信用户写入 req.auth。推荐认证插件直接写入 permissionSubject:
也可以使用简写结构:
安全边界很重要:userId、scope 和 claims 必须来自可信认证结果,不能直接相信请求头、请求体或 URL 参数里的用户/租户自报值。
2. 注册 permissionPlugin
最简单、最好排查的写法是直接传入宿主数据库实例:
这里发生了两件事:
- Vext 启动时,插件创建并初始化
PermissionCore,然后暴露app.permission。 - Vext 关闭时,插件只关闭它自己创建的
PermissionCore,不会关闭宿主的 MonSQLize。
routes 是可选的,但推荐在业务 API 前缀上统一开启权限:
这表示 /api/** 默认都要检查接口权限,/api/auth/** 和 /api/health 明确公开。是否开启权限由服务端配置决定,不由前端请求头决定。
data 是可选的。不开启时,handler 仍可做路由权限;开启 transparent 后,受保护请求里的 app.db.collection() 和 app.db.model() 会自动走权限保护。这里不用手动写 resource: 'db:orders'。collection('orders') 默认访问宿主的 orders collection,并自动推导权限资源 db:orders。
只有物理 collection 名和权限资源名不一致,或某个 collection 需要单独 scope 映射时,才写 collections 覆盖:
authPlugin 默认是 authentication。如果你的认证插件不是这个名字,再显式配置:
3. 路由默认保护和单路由覆盖
推荐用 routes.protect/public 批量声明大部分业务路由,不需要每个接口重复写 permission: true:
如果 /api/orders/:id 命中 routes.protect: ['/api/**'],插件会自动要求当前用户拥有:
这样请求 /api/orders/42 时,插件会用路由模板 api:GET:/api/orders/:id 检查权限,而不是用具体 URL api:GET:/api/orders/42。
单路由仍然可以覆盖:
4. 给角色授权接口权限
路由被 routes.protect 命中,或单独写了 permission: true 以后,还需要给角色授予对应 API 权限:
5. 业务 CRUD 继续使用 app.db
如果接口要返回数据库里的订单,启用 data.transparent 后,受保护请求里的 app.db.collection() 会自动变成权限保护后的 collection:
如果你的项目把查询放在 Vext service 里,也可以继续用 this.app.db:
使用 Vext model 层时,基础 CRUD 也可以这样写:
这段代码里每个参数的含义:
角色除了路由 invoke,还需要数据资源 read:
如果当前 subject 的 scope 是 { tenantId: 'acme' },并且 scopeFields.tenantId 配成文档字段 tenantId,那么查询会自动限定 tenantId = 'acme'。用户没有 read + db:orders、filter 不安全、字段不可读或 scope 字段没配置时,请求会 fail closed。
注意:app.db.use(...)、app.db.pool(...)、model 的 raw()、集合/索引管理、aggregate()、watch() 等高级能力在受保护请求中不会透明放行。需要这些能力时,应该在服务端明确设计资源、规则和审计边界,而不是默认绕过权限。
6. 请求结果如何判断
这就是插件的稳定性策略:宁可拒绝,也不在权限状态不确定时继续放行。
7. 响应字段投影(需要时)
字段权限不是写在 handler 里的。先在管理端保存这个 API 允许返回哪些字段:
这表示 /api/orders 返回 { items, total } 时,只裁剪 items 里的字段,total 保留。保存后,再把字段权限分配给角色;具体分配流程见接口与响应字段。
如果路由被权限保护(来自 routes.protect 或单路由 permission),并且 handler 通过 res.json() 返回数据,插件会自动按默认 api:METHOD:/path 资源执行响应字段投影,并写入:
手动裁剪时这样写:
受权限保护的路由不能开启共享缓存。插件检测到受保护路由启用缓存时,会以 VEXT_ROUTE_PERMISSION_INVALID 拒绝启动,避免把某个用户的响应裁剪结果缓存给其他用户。
8. 额外权限(需要时)
一个路由声明多个权限要求
大多数接口命中 routes.protect 就够了。只有需要组合权限时,才在单路由使用对象形式:
省略 resource 时,默认使用当前路由的 api: 资源,因此这里的 { action: 'export' } 表示 export + api:POST:/api/orders/export。mode: 'all' 表示全部满足;mode: 'any' 表示满足任意一个。组合项最多 32 个。
这适合静态权限:路由一进入 handler 前就要同时满足 invoke 和 export。普通 collection/model 读写仍然继续使用 app.db。
handler 里动态检查额外权限
只有额外权限取决于 handler 里的业务条件时,才读取请求权限上下文。例如同一个审批接口里,超过某个金额才要求 approve-large-order:
requirePermissionContext(req) 返回当前请求专用的 { subject, can, assert, filterResponse }。路由默认保护已经检查过 invoke + api:POST:/api/orders/:id/approve;handler 里的 assert() 只是追加动态条件。不要把它当成读取 db:orders 的普通写法,也不要跨请求缓存这个对象。
9. 高级接入选项
默认建议直接传 monsqlize。只有宿主架构需要插件间解析时,才考虑下面的选项:
这些选项三点要注意:
monsqlize、resolveMonSQLize(app)和自动发现app.monsqlize是三种数据库来源,不能混用。databasePlugin只负责插件排序,不会替你创建数据库连接。subject.resolve(req)只能读取可信认证对象和宿主上下文,不能相信客户端自报的身份。routes.protect/public来自服务端配置;不要让前端请求头决定是否启用或绕过权限。data.transparent只在受保护请求上下文中增强app.db;非请求上下文、后台任务和公开路由仍走宿主原始 DB。
稳定性与关闭边界
- MonSQLize 缺失、不兼容、扩展冲突、路由权限元数据无效:阻止启动。
- 启动后路由图变化:返回
VEXT_ROUTE_RESTART_REQUIRED(503),直到冷重启。 - 受保护路由开启共享缓存:阻止启动。
req.auth.permission和透明app.db授权结果:只属于当前请求,不能跨请求缓存。- Vext 关闭:插件排空并关闭 PermissionCore;宿主数据库仍由宿主关闭。
运行 Vext 示例 可以看到完整的 200/401/403/503 结果;全部选项和类型见 Vext 插件 API。