每个业务模块必须使用独立数据库和独立账号,mongodb权限仅支持数据库粒度,跨库授权不可靠;连接串的database参数必须与账号绑定的db完全一致,否则认证失败;禁用anydatabase角色,避免前缀模拟逻辑库,租户删除需同步清理数据库。

每个业务模块必须用独立数据库 + 独立账号
只靠一个数据库配多个用户,根本做不到隔离——MongoDB 的权限系统是「数据库粒度」绑定的,不是「集合」或「前缀」粒度。你给用户授予 readWrite 角色时,如果不指定 db 参数,它默认落在 admin 库,权限实际作用于整个集群。
正确做法是:为 finance_svc、logistics_svc、marketing_svc 各建一个同名数据库,再在各自库上下文中创建专属账号:
- 先
use finance_svc,再执行db.runCommand({ createUser: "fin_user", pwd: "...", roles: [{ role: "readWrite", db: "finance_svc" }] }) - 绝不能在
admin库里执行createUser却把roles.db设为"finance_svc"——这是跨库授权,运维难审计、迁移脚本易错 - 角色必须用数据库级角色(如
readWrite),禁用dbAdminAnyDatabase、clusterAdmin等任何带AnyDatabase后缀的角色
连接串 database 参数必须和账号绑定的 db 完全一致
哪怕账号权限配置得再严,连接串写错就直接失效。MongoDB 认证时会把连接 URL 路径部分(即 /database_name)当作认证上下文,和账号创建时的 db 字段做精确比对。
错误示例:mongodb://fin_user:xxx@host:27017/admin(账号属于 finance_svc,却连 admin)→ 所有操作被拒
正确写法:mongodb://fin_user:xxx@host:27017/finance_svc
- Spring Boot 中对应配置是
spring.data.mongodb.database=finance_svc,不能写成default_database或留空 - Node.js 驱动里传
dbName: "finance_svc"选项;Golang 的mgo.DialWithInfo或官方驱动的Client.Database("finance_svc")必须显式指定 - 如果用 mongosh 连接,
use logistics_svc不会切换权限上下文——权限只由初始连接串决定
避免用 collection 前缀模拟“逻辑库”
有人试图用 finance_users、logistics_orders 这类命名在单个数据库里区分业务,再靠应用层过滤。这看似省事,实则埋雷:
- 无法用 RBAC 实现硬隔离:你没法给用户授“所有以
finance_开头的集合”的权限,resource.db必须写死,resource.collection不支持通配符 -
listDatabases权限虽不泄露数据,但暴露全部库名——如果不用独立库,攻击者扫一遍show collections就能推断出所有业务模块结构 - 备份恢复、监控告警、慢查询分析都按库粒度设计,混在一起会让 SRE 工具链失焦
GridFS 和分片场景下仍要延续库级隔离原则
GridFS 不是例外。即使你用 bucketName 分离文件存储,底层仍是 tenant_finance.files 和 tenant_logistics.files 两个集合——但它们若在同一数据库里,一个拥有 readWrite 权限的用户就能查遍所有 bucket。
所以真正安全的做法是:
- 每个业务模块用独立数据库 →
finance_svc、logistics_svc - 每个库内用独立 GridFS bucket →
db = client.db("finance_svc"); new GridFSBucket(db, { bucketName: "files" }) - 分片集群中,
shardCollection("finance_svc.transactions", { tenant_id: 1 })的分片键仍要配合库级账号,不能指望分片代替权限控制
最容易被忽略的一点:租户删除时,不只是删账号,还得删整个数据库(db.dropDatabase()),否则残留的 finance_svc 库可能被新业务误复用,导致权限策略错位。











