前面的纸舟书店实例只绑定在回环地址上,方便学习数据库操作,但它还不是可直接发布的安全配置。真正的安全边界至少包括网络、传输加密、身份认证、权限控制、密钥管理和持续更新。本节会启动一个独立的认证实例,创建管理员与应用账号,并亲手验证“允许什么、拒绝什么”。
安全配置不是把所有开关都打开。我们要先回答谁会访问数据库、从哪里访问、需要执行哪些操作,以及凭据泄露后攻击者能做什么。
纸舟书店至少有四类主体:
这个表就是最小权限原则的起点。权限应从业务动作推导,而不是因为配置方便就给应用 root。
一条数据库连接会经过多层边界:
网络可达性
↓
TLS:连接对象是否可信、传输是否加密
↓
认证:你是谁
↓
授权:你可以对哪些资源执行哪些动作
↓
审计、日志、告警:发生了什么任何一层都不能替代其他层。只开放防火墙但不认证,网络内的任意进程仍可能读写数据;只配置密码但不使用 TLS,凭据和数据在不可信网络上仍可能暴露。
MongoDB 默认端口是 27017。端口本身不危险,危险的是把未加保护的服务暴露给不受信任的网络。开发阶段把端口绑定为 127.0.0.1:27017:27017,意味着只有同一设备上的进程能通过这个发布端口访问;生产部署还应使用私有子网、安全组或防火墙,只允许应用节点和运维入口访问。
--bind_ip_all 让 mongod 监听所有容器网卡,是容器之间通信的常见需要,但它不等于允许互联网访问。最终可达性由容器端口发布、承载网络、防火墙和云安全策略共同决定。
不要用 0.0.0.0:27017:27017 配合关闭认证的 mongod。即使只是短时间实验,也可能把数据库暴露给同一网络或更广范围内的其他主机。
认证回答“这条连接代表哪个用户”,授权回答“这个用户能执行什么”。MongoDB Community Edition 常用 SCRAM 用户名密码认证;用户保存在某个认证数据库中,所以连接字符串里的 authSource 必须与创建用户的位置相符。
官方容器镜像支持 MONGO_INITDB_ROOT_USERNAME 和 MONGO_INITDB_ROOT_PASSWORD。它们只在数据目录第一次初始化时创建管理员。如果数据卷已经初始化,后来修改环境变量不会重置用户。
本节使用一个不挂载数据卷的独立实例。示例密码只为展示命令结构,使用后会随容器删除;真实项目应由密钥管理系统注入随机凭据。
下面把容器的 27017 发布到回环地址的 27018,避免与主课程容器冲突。环境变量会触发首次初始化并启用认证:
docker run -d \
--name mongo-secure \
-p 127.0.0.1:27018:27017 \
-e MONGO_INITDB_ROOT_USERNAME=course_admin \
-e MONGO_INITDB_ROOT_PASSWORD=CourseOnly_ChangeMe_2026 \
mongo:8.00d1f8cbb8ab6...容器 ID 每次不同。初始化脚本完成前连接可能失败,因此先等待服务器能够接受带认证的 ping:
docker exec mongo-secure mongosh --quiet \
--username course_admin \
--password CourseOnly_ChangeMe_2026 \
--authenticationDatabase admin \
--eval '
assert.soon(
() => db.runCommand({ ping: 1 }).ok === 1,
"MongoDB 未在预期时间内就绪"
);
printjson(db.runCommand({ connectionStatus: 1 }))
'结果中会出现已认证用户:
{
authInfo: {
authenticatedUsers: [
{ user: 'course_admin', db: 'admin' }
],
authenticatedUserRoles: [
{ role: 'root', db: 'admin' }
]
},
ok: 1
}root 适合初始化和受控管理,不适合订单 API。下一步会创建权限更窄的账号。
先验证没有凭据时,读取管理信息会被拒绝:
docker exec mongo-secure mongosh --quiet --eval '
db.adminCommand({ listDatabases: 1 })
'MongoServerError[Unauthorized]: Command listDatabases requires authentication这条错误是预期结果,它证明认证边界正在工作。ping 可能用于连通性检查,不能用它单独证明业务数据已经受到授权保护。
内置角色 readWrite 允许在指定数据库中读写普通集合,但不授予全局用户管理或服务器控制能力。我们把应用用户创建在 bookstore 数据库中,它的 authSource 也就是 bookstore。
真实系统通常还会把读报表、迁移、备份和管理拆成不同账号。这样即使某个连接串泄露,影响范围也受角色限制。
先以管理员身份创建 bookstore_app:
docker exec mongo-secure mongosh --quiet \
--username course_admin \
--password CourseOnly_ChangeMe_2026 \
--authenticationDatabase admin \
--eval '
const appDb = db.getSiblingDB("bookstore");
printjson(appDb.createUser({
user: "bookstore_app",
pwd: "AppOnly_ChangeMe_2026",
roles: [
{ role: "readWrite", db: "bookstore" }
]
}))
'{ ok: 1 }再使用应用账号写入一份最小数据。这里显式指定 authSource=bookstore:
docker exec mongo-secure mongosh --quiet \
'mongodb://bookstore_app:AppOnly_ChangeMe_2026@127.0.0.1:27017/bookstore?authSource=bookstore' \
--eval '
const write = db.books.insertOne({
_id: "secure-book-001",
title: "权限边界",
stock: 3
});
printjson(write);
printjson(db.books.findOne(
{ _id: "secure-book-001" },
{ _id: 1, title: 1, stock: 1 }
))
'{
acknowledged: true,
insertedId: 'secure-book-001'
}
{
_id: 'secure-book-001',
title: '权限边界',
stock: 3
}现在让同一个应用用户尝试读取用户列表:
docker exec mongo-secure mongosh --quiet \
'mongodb://bookstore_app:AppOnly_ChangeMe_2026@127.0.0.1:27017/bookstore?authSource=bookstore' \
--eval '
db.getSiblingDB("admin").runCommand({ usersInfo: 1 })
'MongoServerError[Unauthorized]: not authorized on admin to execute command一条允许结果和一条拒绝结果合在一起,才完整证明角色设计符合预期:应用能完成业务操作,却不能管理 admin 数据库。
权限验收不能只检查“应用能不能跑”。还要主动尝试一个本应失败的高权限动作。拒绝测试成功,才说明边界不是纸面配置。
下面这种 URI 同时包含用户名、密码、地址、认证库和连接选项:
mongodb://bookstore_app:密码@数据库地址:27017/bookstore?authSource=bookstore它不应出现在 Git 仓库、错误页面、构建日志或前端 JavaScript 中。Node.js 项目应从进程环境读取:
const uri = process.env.MONGODB_URI;
if (!uri) {
throw new Error("缺少 MONGODB_URI");
}启动时注入变量:
MONGODB_URI='mongodb://bookstore_app:已编码的密码@db.internal:27017/bookstore?authSource=bookstore' \
node src/server.js如果用户名或密码含有 @、:、/、?、# 等 URI 保留字符,需要进行百分号编码。应用日志应记录连接目标和错误分类,但要删除凭据。生产系统更适合使用密钥管理服务、短期凭据或受限的 Secret 挂载,而不是在 shell 历史里输入长期密码。
SCRAM 证明连接者知道密码,TLS 则加密链路并通过证书帮助客户端确认服务器身份。跨主机连接、云网络和任何不完全受信任的链路都应要求 TLS。
自托管部署的完整流程包括:
mongod 使用 requireTLS、服务器证书和 CA 文件启动。配置文件的核心形状如下,路径应指向受保护的证书文件:
net:
tls:
mode: requireTLS
certificateKeyFile: /etc/mongodb/tls/server.pem
CAFile: /etc/mongodb/tls/ca.pem客户端 URI 会包含 tls=true:
mongodb://bookstore_app@db.internal:27017/bookstore?authSource=bookstore&tls=true不要为了让自签名证书“先跑起来”就长期使用 tlsAllowInvalidCertificates 或关闭主机名校验。它们会削弱客户端确认服务器身份的能力。证书生成、文件权限和滚动轮换应按 MongoDB 官方的 TLS 配置文档以及组织的 PKI 规范完成。
对自托管副本集,只保护客户端认证还不够。成员彼此也要确认对方属于同一集群。Community Edition 可以使用 keyfile;更严格的企业环境可能使用 x.509。keyfile 是共享秘密,应具有严格文件权限,并通过安全渠道分发。
启用 keyfile 会隐式启用访问控制,因此正确顺序通常是:
security.keyFile。生产副本集的 TLS、内部认证、DNS、证书轮换和无停机变更彼此相关,不适合靠一条复制粘贴命令完成。课程把原理和验收边界讲清,真正部署时应以对应服务器版本的安全检查清单为准。
即使账号只有 readWrite,攻击者仍可能通过应用允许的查询接口消耗大量资源或读取过多数据。API 不应把客户端 JSON 原样当作 MongoDB 过滤器,也不应允许任意聚合阶段、任意排序字段或无限制的 limit。
纸舟书店 API 可以建立明确边界:
title、category 和受限的价格范围。$ 开头的未知操作符。授权限制“账号能碰哪些资源”,输入验证限制“这次请求能表达哪些查询”。二者都需要。
本节没有挂载数据卷,删除容器就会删除它的数据层。先确认需要保留的只是主课程容器 paperboat-mongo,然后执行:
docker rm -f mongo-securemongo-securedocker ps -a --filter name=^/mongo-secure$ --format '{{.Names}}'没有输出,说明 mongo-secure 已删除。主课程容器没有被这条按名称过滤的命令影响。
上线前至少逐项回答:
安全不是一次配置完成的状态。版本更新、人员变更、新服务接入和数据范围变化都会改变边界,清单需要定期复核。