最安全的租户隔离方式是每个租户一个独立数据库并配专属账号,因MongoDB原生权限仅在此组合下能在服务端硬拦截越界访问;其他方案均依赖应用层严格校验tenant_id,漏写即导致数据越权。

租户隔离最安全的落地方式,是“每个租户一个数据库 + 每个数据库一个专属账号”,其他方案都得靠应用层拼命兜底,一漏就出事。
为什么必须为每个租户创建独立数据库和账号
这是唯一能让 MongoDB 原生权限系统在服务端拦住越界访问的组合。admin 库里的用户、跨库角色、共享数据库下的 tenant_id 过滤——全都有绕过风险。
- 用户必须在目标租户库上下文中执行
createUser,比如租户库叫acme_001,就得先use acme_001再运行命令 -
createUser的db参数必须与库名严格一致,写成db: "acme_001";若在admin库里执行却把roles指向acme_001,属于跨库授权,运维易错、审计难查 - 禁止授予
root、clusterAdmin、dbAdminAnyDatabase等高危角色;租户账号只应有readWrite或必要时dbOwner - 连接串的 database 路径(如
mongodb://u:p@h:27017/acme_001)必须和账号绑定的db一致,否则认证上下文错位,操作直接被拒
逻辑隔离(单库多租户)必须强制 tenant_id 过滤
如果因租户量大或成本限制只能用单数据库+tenant_id 字段,那所有读写操作都得显式带条件,MongoDB 不会帮你补。
- 查询、更新、删除操作漏写
{ tenant_id: "xxx" },就会跨租户污染数据;事务里漏写更危险,因为原子性会让错误操作“稳稳提交” - Mongoose 等 ODM 必须用
pre('find')、pre('updateOne')钩子校验tenant_id是否存在且非空,不能只靠中间件 - 聚合管道中
$lookup无法动态指定集合名,硬编码目标集合后,必须在后续$match阶段手动加tenant_id条件,且不能漏 - 索引必须以
tenant_id为前导字段,例如db.orders.createIndex({ tenant_id: 1, created_at: -1 }),否则过滤失效,可能触发内存超限错误
分片键选 tenant_id 但别碰 Zone Sharding
想靠分片提升租户数据局部性?可以,但必须用 tenant_id 作分片键,而不是套用 Zone Sharding。
-
sh.shardCollection("db.coll", { tenant_id: 1 })(范围分片)或{ tenant_id: "hashed" }(哈希分片)才能让同一租户数据物理聚拢 -
sh.addShardToZone("shard01", "tenant-a")这类 Zone 操作本质是控制物理位置,不提供任何租户语义或访问控制,配置链长、增删租户成本高、完全不防越权查询 - 已有集合分片前,必须先建好
{ tenant_id: 1 }索引,否则sh.shardCollection()会失败;空集合可用numInitialChunks预分片,非空集合得手动moveChunk - 分片键结构要和
sh.updateZoneKeyRange()的范围严格对齐,比如分片键是{ tenant_id: 1, createdAt: 1 },范围就得写成{ tenant_id: "a", createdAt: MinKey }到{ tenant_id: "a", createdAt: MaxKey }
迁移与自动化脚本最容易翻车的地方
人工一个个建库建号不可持续,但脚本写错一条,就可能批量创建出权限失控的账号。
- 用
golang-migrate或类似工具管理账号创建时,脚本不能写死;必须按租户动态生成,包括库名、用户名、密码、db参数、roles中的db字段 - 密码必须动态生成(如 32 位随机字符串),严禁硬编码或复用;脚本执行日志要保留完整参数,方便审计
- Spring Boot 配置里
spring.data.mongodb.database必须设为具体租户库名,不能是default或占位符;否则连接池初始化时就会连错库 - 备份恢复也得按租户粒度做:
mongodump -d acme_001,不能 dump 整个实例;误删集合时无法靠collMod恢复,只能从对应租户备份里捞
真正难的不是写对第一条命令,而是确保所有租户生命周期操作(开通、扩容、迁移、下线)都走同一套受控流程——任何跳过脚本、手工干预、配置绕过的环节,都是隔离墙上的裂缝。


















