必须显式在所有查询中带上tenantid条件,漏写一次即导致跨租户数据泄露;需为tenantid设计前导复合索引、复合唯一约束,并在聚合$lookup子管道及gridfs动态bucketname中严格对齐该规则。

所有查询必须显式带上 tenantId 条件
漏写一次 tenantId 过滤,就可能让租户 A 读到或删掉租户 B 的数据。MongoDB 本身不校验租户上下文,find、updateOne、deleteMany、aggregate 全部要手动加 { tenantId: "xxx" }。
常见错误现象:后台定时任务、导出脚本、管理后台的“全部查询”按钮没加 tenantId,结果批量操作跨租户;$lookup 关联其他集合时只在外层加了条件,子管道里没重复加,导致关联结果混杂。
- 不能依赖中间件或拦截器自动注入 —— 中间件可能被绕过(如直连 shell、CLI 工具、测试脚本)
- Mongoose 可用
pre('find')和pre('save')钩子强制检查tenantId是否存在且非空 - 聚合管道中每个
$lookup的pipeline字段里也得单独写{ tenantId: "$$tenantId" },并确保外层传入let: { tenantId: "$tenantId" }
tenantId 必须作为索引前导字段
哪怕写了 { tenantId: "abc123" },如果索引是 { createdAt: -1, status: 1 },MongoDB 仍会全表扫描。索引设计不是“有就行”,而是“顺序决定是否生效”。
使用场景:高频按租户查订单、按租户+状态查工单、按租户+时间范围查日志。
- 复合索引必须把
tenantId放第一位,例如db.orders.createIndex({ tenantId: 1, status: 1, createdAt: -1 }) - 唯一约束必须复合,比如防止邮箱重复:建
{ email: 1, tenantId: 1 },而不是只对email建唯一索引 - 如果
tenantId基数很低(比如只有 5 个大客户),单字段索引效率极差,务必避免
集合命名别带租户标识,但 tenantId 类型要统一
不要创建 orders_tenant_abc、orders_tenant_xyz 这类动态集合名。看似隔离,实则引发 namespace 爆炸、聚合受限、备份脚本脆弱等问题。
真正关键的是字段一致性:所有集合里的 tenantId 必须同类型(全用 ObjectId 或全用 string),混用会导致索引失效、比较行为异常。
- 集合名保持业务语义,如
orders、users、files,不要掺租户信息 - 文档插入时强制校验
tenantId存在,可用 MongoDB 5.0+ 的$jsonSchema规则:{ required: ["tenantId"], properties: { tenantId: { bsonType: "string" } } } - 如果租户 ID 来自外部系统(如 UUID 或域名),入库前做规范化处理(如转小写、去特殊字符),避免因格式差异导致查询失败
GridFS 多租户必须用独立 bucketName,不能靠 metadata.tenantId
用默认 fs bucket + metadata.tenantId 过滤,和普通集合漏写 tenantId 一样危险。直连数据库或索引缺失时,文件直接裸露。
正确做法是每个租户对应一个物理隔离的 bucket,比如 tenant_acme_files,它会生成独立的 tenant_acme_files.files 和 tenant_acme_files.chunks 集合。
- 创建
GridFSBucket实例时必须动态传入bucketName,不能复用全局单例 -
bucketName需白名单校验:只允许字母、数字、下划线、短横线,且不能以数字开头 - 权限必须落到集合级 —— 给租户账号授予
tenant_acme_files.files和tenant_acme_files.chunks的读写权,禁用readWrite数据库角色











