MongoDB内置角色仅提供数据库操作级权限,无法满足“只删自己订单”等业务语义级控制,必须通过应用层建模+自定义权限系统实现。

MongoDB 自带的 readWrite、dbAdmin 这类角色只管“能不能执行 find”,不管“能不能删 user:123 的订单”——业务权限必须自己建模,不能硬套内置角色。
为什么不能直接用 MongoDB 内置角色做业务授权
内置角色是数据库操作级权限,不是业务语义级权限。比如你给用户分配了 readWrite 到 orders 库,他就真能删掉所有订单,包括别人的;而业务上往往要求“只能删自己创建的”。这种规则 MongoDB 原生不支持,必须靠应用层配合自定义数据模型实现。
-
root或readWriteAnyDatabase在生产环境属于高危配置,审计时基本过不了 - 用
$where或聚合管道动态过滤文档,性能差、无法走索引,且容易被绕过 - 权限判断逻辑如果散落在各处(比如每个 API 都手写
if (user.id === doc.ownerId)),后期改规则时漏改一处就出漏洞
RBAC 文档结构怎么设计才支持多级继承和动态权限计算
关键不是“存多少字段”,而是“怎么让聚合查得快、更新影响小、继承关系不闭环”。推荐扁平化 + 图遍历组合方案:
- 角色文档里用
parent_id字段表示直接上级,不要嵌套children数组(否则增删节点要双写) - 权限字段统一用字符串数组,例如
["post:read", "post:delete:own"],不用嵌套对象或布尔 map - 用户文档中只存直接角色 ID 数组
roleIds: [ObjectId("..."), ObjectId("...")],不存展开后的权限快照 - 查某用户是否有
post:delete权限时,必须用$graphLookup一次性拉出所有祖先角色,再$unwind+$group合并权限数组
示例聚合片段:
db.users.aggregate([
{ $match: { username: "alice" } },
{ $lookup: { from: "roles", localField: "roleIds", foreignField: "_id", as: "directRoles" } },
{ $unwind: "$directRoles" },
{ $graphLookup: {
from: "roles",
startWith: "$directRoles.parent_id",
connectFromField: "parent_id",
connectToField: "_id",
as: "inheritedRoles",
maxDepth: 5
}
},
{ $project: { allRoutes: { $setUnion: ["$directRoles.routes", "$inheritedRoles.routes"] } } },
{ $match: { "allRoutes": { $in: ["post:delete"] } } }
])
权限校验时最容易忽略的三个坑
很多团队在上线后才发现权限失效或误放行,问题通常出在这几个细节:
-
$graphLookup的startWith字段如果可能为null,必须包一层$ifNull,否则整条 pipeline 中断,查不到任何结果 - 角色变更后,Redis 里缓存的用户权限快照(如键
perm:u:123)不会自动失效——必须在更新角色时触发清理,或加 TTL 并配合定时校验脚本 - 权限字符串设计别用通配符,比如
"post:*"或"/api/v1/posts/**",MongoDB 不支持 glob 匹配,应用层解析规则不一致会导致绕过
聚合查询权限要不要加索引?怎么加
要加,而且必须加对位置。光建 { roleIds: 1 } 没用,因为 $lookup 和 $graphLookup 实际查的是 roles 集合的 _id 和 parent_id 字段。
- 在
roles集合上建复合索引:{ _id: 1, parent_id: 1 },覆盖$graphLookup的connectToField和connectFromField - 在
users集合上建索引:{ roleIds: 1 },加速$lookup的本地匹配 - 每次上线新权限逻辑前,务必跑
.explain("executionStats")看是否命中索引,特别是totalDocsExamined是否接近 0
真正麻烦的从来不是“怎么写权限代码”,而是“怎么确保每次角色变更都原子性地同步到所有依赖方”,这个链路一旦断裂,安全边界就消失了。











