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 的三个要点:

  1. 需要一个有效的 CA 签发的证书(内网可用自签 CA)。
  2. 服务端证书的 CN 或 SAN 必须与连接串中的主机名匹配。
  3. 启用 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 安全配置适用

  1. 生产环境所有 MongoDB 实例——必须开启认证和最小权限。
  2. 需要多团队协作的项目——不同账号不同权限,互不干扰。
  3. 数据合规要求(GDPR/PIPL)——传输加密 + 字段级加密 + 审计。
  4. 监控和备份系统的账号隔离——专用账号避免权限泄漏。
  5. CI/CD 管道中的临时数据库——动态创建账号并在管道结束时销毁。

不适用场景

  1. 纯本机单机学习环境——开认证增加心智负担,但关闭网络端口。
  2. 只读的数据副本——如果不需要区分用户,可以用 --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 思考题

  1. 如果一个用户被授予了 readWrite 在 database_A,又被授予了 read 在 database_B,他能跨库做 $lookup 从 database_A 关联 database_B 的数据吗?
  2. MongoDB 的客户端字段级加密(CSFLE)是如何实现"数据库管理员也看不到明文"的?加密和解密在哪里发生?

(答案将在第 14 章末尾揭晓)


上一章思考题答案

  1. Spring Data MongoDB 的分页 Page<Product> 底层使用的是 skip + limit,而非游标分页。它在构造 Query 时自动加入 .skip(page * size).limit(size),不涉及游标编码/解码。这也是为什么用 Spring Data 默认分页翻到深页性能会急剧下降——skip 机制的本质问题无法绕过。若需高性能深分页,必须手动实现游标分页(用 MongoTemplate 构造 $gt/$lt 条件)。

  2. Spring Boot 的读写分离配置:在 MongoClientSettings 中设置 readPreference(ReadPreference.secondary())。一致性陷阱——写后立刻读可能从 Secondary 读不到刚写的数据(未复制完成);Secondary 延迟较大时可能读到过期数据。对策:要求强一致性的读操作在 MongoTemplate 查询时临时覆写 readPreferenceprimary(但会导致读写分离失效,该查询的流量回到主库)。

延伸阅读与资源

MongoDB 实战进阶与内核修炼
python入门:Rquests从菜鸟脚本到企业级SDK的网络实战圣经
Milvus向量数据库实战修炼:从 0 到 1精通向量检索与生产落地
后端工程师的 AI 转型第一课:Ollama 与私有化大模型实战
10倍开发者的 Dify 魔法书:从零构建全栈 AI 应用
后端工程师转型AI第一课-Ollama 与私有化大模型实战

大型语言模型(LLM) vLLM 高性能推理落地实战

Agent开发之LlamaIndex 实战修炼与源码进阶

大语言模型Transformers 实战修炼与源码剖析

posted on 2026-07-26 10:26  一天不进步,就是退步  阅读(4)  评论(0)    收藏  举报