1. 项目背景
业务场景:本地生活电商上线 6 个月,运行平稳——直到某天凌晨 3 点,运维被告警电话吵醒。一个外包开发离职前把自己的 MongoDB 账号分享到了公共 GitHub 仓库,黑客用这个账号在凌晨删除了 3 个核心集合,还在一个集合里留下了勒索信息"支付 0.5 BTC 到以下地址,否则数据永久丢失"。更可气的是,安全审查发现:所有服务共用一个 root 账号,没有区分读写权限;开发环境的 MongoDB 端口 27017 直接暴露在公网上,没有任何防火墙;TLS 连接加密从未开启——数据传输都是明文。
痛点:MongoDB 默认安装下没有任何安全机制,不配置等于"裸奔"。常见安全隐患:使用 root 账号做所有操作——一个应用挂掉误写了 admin 库,整个数据库集群权限被破坏;所有团队共用密码、明码写在配置文件中;不区分开发/测试/监控账号,监控系统用的也是 root;数据库端口没有 IP 白名单,任何人都能尝试连接;从 2017 年起有多起大规模的 MongoDB 勒索攻击,就是利用未认证的 MongoDB 实例直接删除数据并勒索。
2. 项目设计:剧本式交锋对话
小胖(脸色苍白地看新闻):大师!我刚看到一篇报道——一家公司 MongoDB 没设密码,被黑客删库勒索了 0.5 个比特币!我们的 MongoDB 是不是也很危险?
大师:你们目前用的是什么账号?
小胖:呃……admin / admin123。
大师(叹气):那就是裸奔。MongoDB 安全有三个基本防线——认证(你是谁)、授权(你能做什么)、加密(你的数据在路上安不安全)。目前你们三道防线全没守住。
小胖:认证和授权有区别吗?登录不就是确定权限吗?
大师:是两步。"认证"是 MongoDB 回答"你是谁",通过用户名密码或证书完成;"授权"是回答"你能做什么",通过角色(Role)来管理。认证成功后,你的操作被分配给角色的权限框架校验。
技术映射:MongoDB 的认证默认使用 SCRAM(Salted Challenge Response Authentication Mechanism),密码不会以明文形式在网络上传送。授权基于 RBAC(Role-Based Access Control),每个用户被授予一个或多个角色,角色定义了哪些操作可以在哪些数据库/集合上执行。
小白:我看到 MongoDB 有很多内置角色——read、readWrite、dbAdmin、clusterMonitor、root……这些怎么区分?
大师:画张表最清楚:
| 角色 | 权限范围 | 适用对象 |
|---|---|---|
read |
仅读取指定数据库 | 只读报表、数据分析师 |
readWrite |
读写指定数据库 | 业务应用服务 |
dbAdmin |
管理数据库(索引、统计、清理) | DBA(不含读写数据) |
userAdmin |
管理用户和角色 | 安全管理员 |
clusterAdmin |
管理集群(复制集、分片) | 运维/DBA |
clusterMonitor |
查看集群状态(无写权限) | Prometheus 监控 |
backup / restore |
备份和恢复 | 备份系统专用账号 |
root |
所有权限 | 仅超级管理员,禁止日常使用 |
小胖:那最小权限原则就是——开发用 readWrite,监控用 clusterMonitor,备份用 backup,日常操作绝不给 root?
大师:一针见血。还有一个技巧:账号分层。每一层(开发环境、测试环境、预发布、生产环境)用不同的用户名和密码,不要复用。开发环境的密码泄露不会影响生产环境。
技术映射:db.createUser() 创建用户,db.grantRolesToUser() 追加角色,db.revokeRolesFromUser() 撤销角色。第 3 章讲过的 authenticationDatabase 就是指用户被定义在哪个库——root 和系统级用户在 admin 库,业务用户在各自业务库。
小白:那 TLS/SSL 连接加密呢?是不是配个证书就行了?
大师:原理跟你 HTTPS 访问网站一样——客户端和服务端各自持有证书,建立加密通道。MongoDB 的 TLS 可以双向认证(客户端也需证书),但多数项目只做单向(服务端出示证书)。配置 TLS 的三个要点:
- 需要一个有效的 CA 签发的证书(内网可用自签 CA)。
- 服务端证书的 CN 或 SAN 必须与连接串中的主机名匹配。
- 启用 TLS 后,端口不变(27017),但传输内容加密。
小胖:还有个问题——MongoDB 偶尔曝出严重漏洞(比如 CVE),我们怎么知道什么时候该打补丁?
大师:这属于安全运维的持续过程。建议订阅 MongoDB 的安全公告邮件;生产环境用 MongoDB 的稳定版本(偶数版本号如 7.0、8.0),避开刚出的 .0 版本;Docker 部署的好处是可以快速滚动升级镜像版本而不停服务(配合复制集)。
大师(总结):今天的三件事——建账号分层体系,给每个服务/角色独立的用户名和最小权限;如果只是在内网跑,至少也要开 SCRAM 认证;如果数据是用户的 PII(个人信息),TLS 加密是底线。明天开始,先把 admin123 改掉。
3. 项目实战
3.1 环境准备
沿用第 2 章的 Docker MongoDB,它已在 docker-compose.yml 中配了 root 用户。
3.2 分步实现
步骤一:创建分层账号体系
目标:为开发、测试、只读报表、监控系统创建独立账号,遵循最小权限原则。
// 使用 root 账号连接
use admin
// 1. 创建业务应用账号(local_life 库的读写权限)
db.createUser({
user: "app_life",
pwd: "App_Life_P@ss_2026",
roles: [
{ role: "readWrite", db: "local_life" } // 仅在 local_life 库有读写
]
})
// 2. 创建只读报表账号
db.createUser({
user: "report_reader",
pwd: "Report_Read_Only_2026",
roles: [
{ role: "read", db: "local_life" } // 仅可读取
]
})
// 3. 创建测试账号
db.createUser({
user: "tester_life",
pwd: "Test_Life_2026",
roles: [
{ role: "readWrite", db: "local_life_test" }
]
})
// 4. 创建监控专用账号(不读写数据,只看集群状态)
db.createUser({
user: "monitor",
pwd: "Monitor_2026",
roles: [
{ role: "clusterMonitor", db: "admin" }, // 查看集群指标
{ role: "readAnyDatabase", db: "admin" } // 可读所有库的元数据(可选)
]
})
// 5. 创建备份专用账号
db.createUser({
user: "backup_user",
pwd: "Backup_2026_Secure",
roles: [
{ role: "backup", db: "admin" },
{ role: "restore", db: "admin" }
]
})
// 查看所有用户
db.getUsers()
步骤二:验证权限边界
目标:用不同账号连接并尝试越权操作,验证权限正确生效。
// == 场景1:报表账号尝试写入 ==
// 用 mongosh 连接 report_reader
// mongosh "mongodb://report_reader:Report_Read_Only_2026@localhost:27017/local_life?authSource=admin"
// 尝试读取(应成功)
use local_life
db.products_idx.findOne()
// → 应该成功返回
// 尝试写入(应失败)
db.products_idx.insertOne({ name: "越权测试" })
// → 错误: not authorized on local_life to execute command { insert: "products_idx" ... }
// == 场景2:测试账号尝试读写 local_life 库 ==
// 连接 tester_life
// mongosh "mongodb://tester_life:Test_Life_2026@localhost:27017/local_life?authSource=admin"
use local_life
db.products_idx.findOne()
// → 错误: not authorized on local_life(该账号只有 local_life_test 的权限)
// == 场景3:监控账号尝试读写数据 ==
// mongosh "mongodb://monitor:Monitor_2026@localhost:27017/admin?authSource=admin"
use local_life
db.serverStatus() // 查看服务器状态(应成功,因为 clusterMonitor 角色有权限)
db.products_idx.findOne()
// → 错误: not authorized(clusterMonitor 没有数据读写权限)
步骤三:密码安全——修改密码与密钥轮换
目标:学习修改密码、吊销账号、启用密码检查。
// 使用 root 账号连接
use admin
// 修改密码
db.changeUserPassword("app_life", "App_Life_N3w_P@ss_2026Q2")
// 验证新密码生效
// 用旧密码连接 → 应失败
// 用新密码连接 → 应成功
// 吊销账号权限(保留用户但撤销角色)
db.revokeRolesFromUser("tester_life", [
{ role: "readWrite", db: "local_life_test" }
])
// 完全删除用户
db.dropUser("tester_life")
// 启用密码复杂度检查(MongoDB 企业版)
// MongoDB 社区版不内置密码策略,需在应用层做或使用外部认证
// Atlas 云服务默认有密码强度校验
步骤四:为应用账号授予自定义角色
目标:创建自定义角色——允许在商品集合上执行特定操作。
use local_life
// 创建自定义角色——商品管理员
db.createRole({
role: "productManager",
privileges: [
{
resource: { db: "local_life", collection: "products" },
actions: ["find", "insert", "update", "remove"]
},
{
resource: { db: "local_life", collection: "products" },
actions: ["createIndex", "dropIndex"]
}
],
roles: [] // 不继承其他角色
})
// 将自定义角色授予用户
db.createUser({
user: "product_admin",
pwd: "Product_Admin_2026",
roles: [
{ role: "productManager", db: "local_life" }
]
})
// 验证:product_admin 只能操作 products 集合
// 连接 product_admin
// mongosh "mongodb://product_admin:Product_Admin_2026@localhost:27017/local_life?authSource=local_life"
use local_life
db.products.insertOne({ name: "权测试" }) // → 应成功(products 集合)
db.users.findOne() // → 应失败(无 users 集合权限)
步骤五:连接加密——自签 TLS 证书配置(概述)
目标:了解 MongoDB TLS 加密的配置流程(生产环境需正规 CA 证书)。
# TLS 配置流程(以自签证书为例,仅在本地测试环境使用)
# 1. 生成 CA 私钥和证书
openssl genrsa -out ca.key 2048
openssl req -x509 -new -nodes -key ca.key -sha256 -days 365 -out ca.crt \
-subj "/CN=MongoDB-Test-CA"
# 2. 生成服务端私钥和证书签名请求
openssl genrsa -out mongod.key 2048
openssl req -new -key mongod.key -out mongod.csr \
-subj "/CN=localhost"
# 3. 用 CA 签发服务端证书
openssl x509 -req -in mongod.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out mongod.crt -days 365 -sha256
# 4. 合并证书和私钥为 PEM 文件
cat mongod.key mongod.crt > mongod.pem
chmod 600 mongod.pem
# 5. 启动 mongod 时指定证书
# 在 docker-compose.yml 中增加:
# command: --tlsMode requireTLS --tlsCertificateKeyFile /etc/ssl/mongod.pem --tlsCAFile /etc/ssl/ca.crt
# 并将证书文件挂载进容器
# 6. 客户端连接时指定 TLS
# mongosh --tls --tlsCAFile ca.crt --host localhost
注意:TLS 配置较复杂且环境的差异大,本章仅展示流程框架。生产环境强烈建议使用 MongoDB Atlas(云服务自动配 TLS)或让运维团队通过 Kubernetes Secret 管理证书。
步骤六:安全检查清单
目标:输出一份团队自检用的安全清单。
// MongoDB 安全自检脚本(管理员视角)
use admin
// 1. 检查是否开启认证
const authStatus = db.runCommand({ getCmdLineOpts: 1 })
print("认证状态:", JSON.stringify(authStatus.parsed?.security?.authorization || "未开启 ⚠"))
// 2. 列出所有用户及其角色
db.system.users.find({}, { user: 1, roles: 1 }).forEach(u => {
print(`用户: ${u.user}, 角色: ${JSON.stringify(u.roles)}`)
})
// 3. 检查 root 用户个数(应只有 1 个)
const rootCount = db.system.users.countDocuments({ "roles.role": "root" })
print("Root 用户数:", rootCount, rootCount > 2 ? "⚠ 过多" : "PASS")
// 4. 检查是否有空密码用户
const noPwd = db.system.users.find({ "credentials.SCRAM-SHA-256": { $exists: false } }).count()
print("弱认证账号:", noPwd, noPwd > 0 ? "⚠ 存在" : "PASS")
3.3 完整代码清单
| 文件 | 用途 |
|---|---|
mongodb-lab/scripts/ch13-create-users.js |
创建分层账号体系的脚本 |
mongodb-lab/scripts/ch13-verify-permissions.js |
权限验证脚本 |
mongodb-lab/scripts/ch13-custom-role.js |
自定义角色演示 |
mongodb-lab/scripts/ch13-security-checklist.js |
安全自检脚本 |
3.4 测试验证
// 验证脚本(以 root 身份运行)
use admin
// 1. 确认业务账号仅能访问 local_life
const testAuth = db.runCommand({
usersInfo: { user: "app_life", db: "admin" },
showPrivileges: false
})
print("app_life 角色:", JSON.stringify(testAuth.users[0].roles))
// 应只含 {role:"readWrite", db:"local_life"}
// 2. 确认监控账号不能写数据
const testMonitor = db.runCommand({
usersInfo: { user: "monitor", db: "admin" }
})
const monitorRoles = testMonitor.users[0].roles.map(r => r.role)
print("monitor 是否含有 data 角色:",
monitorRoles.some(r => r.includes("readWrite") || r.includes("root")) ? "FAIL" : "PASS")
// 3. 确认测试账号已删除
const testTester = db.system.users.findOne({ user: "tester_life" })
print("tester_life 是否已清理:", testTester === null ? "PASS" : "FAIL")
// 4. 清理(如需)
// db.dropUser("app_life")
// db.dropUser("report_reader")
// db.dropUser("monitor")
// db.dropUser("backup_user")
// db.dropUser("product_admin")
// db.runCommand({ dropRole: "productManager" })
print("\n=== 安全性验证完成 ===")
4. 项目总结
4.1 MongoDB 安全 vs MySQL 安全
| 维度 | MongoDB | MySQL | 说明 |
|---|---|---|---|
| 认证机制 | SCRAM-SHA-256(默认) | mysql_native_password / caching_sha2_password | 两者均支持强认证 |
| 角色管理 | 内置角色 + 自定义角色 | 全局权限 + 库级权限 | MongoDB 角色粒度更灵活 |
| 网络加密 | TLS(配置稍复杂) | TLS | 功能等价 |
| 默认安装安全 | 无认证(4.0 前) | 有初始密码(5.7+) | MySQL 近年改进更明显 |
| 审计日志 | 企业版功能 | 企业版 + 插件 | 社区版两者均无内置审计 |
| 字段级加密 | 客户端字段级加密(CSFLE) | 需应用层实现 | MongoDB 有原生方案 |
4.2 适用场景
MongoDB 安全配置适用:
- 生产环境所有 MongoDB 实例——必须开启认证和最小权限。
- 需要多团队协作的项目——不同账号不同权限,互不干扰。
- 数据合规要求(GDPR/PIPL)——传输加密 + 字段级加密 + 审计。
- 监控和备份系统的账号隔离——专用账号避免权限泄漏。
- CI/CD 管道中的临时数据库——动态创建账号并在管道结束时销毁。
不适用场景:
- 纯本机单机学习环境——开认证增加心智负担,但关闭网络端口。
- 只读的数据副本——如果不需要区分用户,可以用
--auth但配简单密码。
4.3 注意事项
| 注意事项 | 说明 |
|---|---|
| 认证库(authSource) | root 角色在 admin 库,业务用户在对应业务库,连接时必须正确指定 |
| 密码明文 | 配置文件中勿写明文密码,用环境变量或 Secret Manager(Vault/AWS Secrets Manager) |
匿名角色 readAnyDatabase |
小心授予——它允许读所有数据库,含 system 集合 |
| 未加密的公网 MongoDB | 从 2017 年起已有数万起勒索攻击案例,永久不要公网裸奔 |
| TLS 证书过期 | 自签证书有效期设为定期提醒项,过期会导致所有连接中断 |
4.4 常见踩坑经验
故障案例一:authSource 配错导致连接失败
新来的运维在 Connection String 中写了 mongodb://app_life:pwd@host:27017/local_life,没加 ?authSource=admin,结果连接报错 "Authentication failed"。根因:app_life 是在 admin 库中创建的(root/admin 账号都是),而连接串默认以目标数据库 local_life 作为认证库。解决:连接串末尾加 ?authSource=admin,或在 local_life 库中直接创建 app_life 用户。
故障案例二:监控账号权限不足导致 Prometheus 数据缺失
团队创建了 monitor 用户并授予 clusterMonitor,但 Prometheus 的 mongodb_exporter 请求 db.serverStatus() 正常,请求 db.stats() 却报 403。根因:clusterMonitor 允许 serverStatus 但不允许 dbStats(这是数据库级别的操作)。解决:追加 { role: "read", db: "local_life" } 到 monitor 用户——有 read 权限就能执行 dbStats。
故障案例三:密码泄露后未及时吊销导致数据外泄
某项目 GitHub 仓库中误提交了含 MongoDB 密码的 application.yml,3 天后才发现。此时黑客已通过该账号全量导出了用户数据。根因:密码泄露后的响应太慢,且账号有全量 readWrite 权限。解决:事后应对——立即修改密码 + 检查审计日志;事前预防——使用 GitHub Secret Scanning + .gitignore 明文配置文件 + 密码存入环境变量。
4.5 思考题
- 如果一个用户被授予了
readWrite在 database_A,又被授予了read在 database_B,他能跨库做$lookup从 database_A 关联 database_B 的数据吗? - MongoDB 的客户端字段级加密(CSFLE)是如何实现"数据库管理员也看不到明文"的?加密和解密在哪里发生?
(答案将在第 14 章末尾揭晓)
上一章思考题答案:
Spring Data MongoDB 的分页
Page<Product>底层使用的是skip + limit,而非游标分页。它在构造Query时自动加入.skip(page * size).limit(size),不涉及游标编码/解码。这也是为什么用 Spring Data 默认分页翻到深页性能会急剧下降——skip 机制的本质问题无法绕过。若需高性能深分页,必须手动实现游标分页(用 MongoTemplate 构造$gt/$lt条件)。Spring Boot 的读写分离配置:在
MongoClientSettings中设置readPreference(ReadPreference.secondary())。一致性陷阱——写后立刻读可能从 Secondary 读不到刚写的数据(未复制完成);Secondary 延迟较大时可能读到过期数据。对策:要求强一致性的读操作在MongoTemplate查询时临时覆写readPreference为primary(但会导致读写分离失效,该查询的流量回到主库)。
延伸阅读与资源
MongoDB 实战进阶与内核修炼
python入门:Rquests从菜鸟脚本到企业级SDK的网络实战圣经
Milvus向量数据库实战修炼:从 0 到 1精通向量检索与生产落地
后端工程师的 AI 转型第一课:Ollama 与私有化大模型实战
10倍开发者的 Dify 魔法书:从零构建全栈 AI 应用
后端工程师转型AI第一课-Ollama 与私有化大模型实战

微信公众号: 架构师日常笔记 欢迎关注!
浙公网安备 33010602011771号