基于CASL实现Mongoose嵌套文档的权限校验问题
用CASL + Mongoose实现嵌套关联的Document权限校验
我来帮你搞定这个嵌套数据的权限校验问题,结合CASL和Mongoose其实不难,咱们一步步拆解:
1. 先明确CASL的权限规则
首先得在CASL里定义清楚权限逻辑:用户能访问某个Document,当且仅当该Document关联的Vehicle的users列表里包含当前用户ID。规则代码可以这么写:
const { AbilityBuilder, createMongoAbility } = require('@casl/ability'); function defineAbilitiesFor(user) { const { can, build } = new AbilityBuilder(createMongoAbility); // 核心规则:用户可读取关联自己Vehicle的Document can('read', 'Document', { 'vehicle.users': { $in: [user._id] } }); // 顺带保留用户对自己Vehicle的基础权限(如果业务需要的话) can('read', 'Vehicle', { users: { $in: [user._id] } }); return build(); }
这里利用CASL支持嵌套字段查询的特性,直接指向vehicle.users,后续能无缝和Mongoose的查询逻辑结合。
2. 实现Document的权限查询
有两种常用的实现方式,你可以根据业务场景选:
方式一:分步查询(直观易维护)
这种方式先拿到用户拥有的所有Vehicle ID,再用这些ID筛选Document,逻辑清晰,适合数据量不大的场景:
const mongoose = require('mongoose'); const Vehicle = mongoose.model('Vehicle'); const Document = mongoose.model('Document'); async function getUserDocuments(userId) { // 第一步:获取用户有权访问的所有Vehicle ID const userVehicles = await Vehicle.find({ users: { $in: [userId] } }, '_id'); const vehicleIds = userVehicles.map(vehicle => vehicle._id); // 第二步:用CASL权限规则过滤Document const ability = defineAbilitiesFor({ _id: userId }); const accessibleDocs = await Document.find( ability.can('read', 'Document') ? {} : ability.relevantQuery('read', 'Document') ).where('vehicle').in(vehicleIds); return accessibleDocs; }
方式二:聚合查询(一次数据库请求完成)
如果想减少数据库请求次数,可以用MongoDB的聚合管道,直接关联Vehicle集合并完成权限筛选:
async function getUserDocumentsWithAggregation(userId) { const ability = defineAbilitiesFor({ _id: userId }); const baseQuery = ability.relevantQuery('read', 'Document'); const documents = await Document.aggregate([ // 关联Vehicle集合,拿到对应的users字段 { $lookup: { from: 'vehicles', // 注意这里填集合名,不是模型名 localField: 'vehicle', foreignField: '_id', as: 'vehicleInfo' } }, // 过滤掉未关联到Vehicle的无效Document { $match: { vehicleInfo: { $ne: [] } } }, // 展开关联的Vehicle数据,方便后续筛选 { $unwind: '$vehicleInfo' }, // 同时应用CASL规则和用户ID筛选 { $match: { ...baseQuery, 'vehicleInfo.users': { $in: [userId] } } }, // 可选:移除临时关联的Vehicle数据,返回原始Document结构 { $project: { vehicleInfo: 0 } } ]); return documents; }
3. 更优雅的集成:CASL Mongoose插件(可选)
如果你想让代码更简洁,可以用@casl/mongoose插件,给模型直接添加accessibleBy方法:
const { accessibleRecordsPlugin } = require('@casl/mongoose'); // 给Document Schema注册插件 DocumentSchema.plugin(accessibleRecordsPlugin); // 之后查询代码可以简化成这样: async function getUserDocumentsWithPlugin(userId) { const ability = defineAbilitiesFor({ _id: userId }); // 还可以顺便populate关联的Vehicle数据 return await Document.accessibleBy(ability, 'read').populate('vehicle'); }
插件会自动把CASL规则转换成MongoDB能识别的查询条件,省去手动拼接的麻烦。
注意事项
- 确保
userId是ObjectId类型,如果是字符串要先转换:mongoose.Types.ObjectId(userId) - 如果需要支持创建、更新Document的权限,只要调整CASL规则即可,比如:
can('create', 'Document', { vehicle: { users: { $in: [user._id] } } })
内容的提问来源于stack exchange,提问作者Adriano Foschi
相关产品推荐
相关产品推荐

