
1. 从一次权限误操作说起为什么用户管理不能靠“凭感觉”刚接手一个内容管理系统的数据库维护时我犯过一个很典型的错误。当时为了图省事直接给应用连接账号分配了readWriteAnyDatabase角色想着“反正只是读写问题不大”。结果某天排查一个数据异常时发现这个账号不仅能读写业务库还能顺手把系统库里的用户信息、角色配置一并改掉。那一刻我才意识到MongoDB 的权限体系远比“给个用户名密码”复杂得多它有一套完整的认证、授权、角色继承机制只是很多人包括当时的我从来没认真对待过。这篇文章想聊的就是 MongoDB 里跟“用户”和“权限”相关的那些操作。它适合两类人一类是刚接触 MongoDB、还在用默认无认证方式连接数据库的开发者另一类是已经在生产环境跑着 MongoDB但对权限分配心里没底、想系统梳理一遍的运维或后端同学。我会从认证机制的底层逻辑讲起把内置角色、自定义角色、用户创建与回收、权限验证这些环节拆开揉碎再补上几个我在实际项目里踩过的坑。读完你至少能做到知道该给不同用途的账号分配什么角色、能自己写一个最小权限的自定义角色、遇到“认证失败”或“权限不足”时有一套可复现的排查路径。需要先说明一点MongoDB 的权限模型在不同大版本之间有过调整比如 3.x 到 4.x 在认证机制和角色粒度上都有变化。下面涉及具体命令和角色名的地方我以 4.x 及之后的常见版本为基准如果你用的是更早的版本个别角色名可能对不上需要对照官方文档确认。另外所有示例里的库名、用户名、密码都是虚构的你照着敲的时候记得换成自己的。2. 认证与授权的底层逻辑先搞清楚 MongoDB 怎么“认人”2.1 认证和授权是两件事别混为一谈很多人把“登录”和“能干什么”当成一回事其实在 MongoDB 里这是两个独立阶段。认证Authentication解决的是“你是谁”也就是验证连接方提供的凭据是否有效授权Authorization解决的是“你能做什么”也就是认证通过之后这个身份被允许执行哪些操作。一个账号可能认证成功但执行某个命令时被拒绝这就是授权环节在起作用。MongoDB 支持多种认证机制常见的有SCRAM-SHA-1、SCRAM-SHA-256企业版还有基于证书的MONGODB-X.509以及对接外部认证系统的LDAP、Kerberos等。社区版默认用的是 SCRAM 系列它本质上是一种挑战-响应式的密码认证密码不会以明文在网络里传输。你在创建用户时如果不指定机制MongoDB 会默认用 SCRAM-SHA-1 和 SCRAM-SHA-256 同时生成凭据客户端连接时自动协商用哪个。这里有个容易被忽略的点开启认证之后本地连接localhost不再享有“免检”特权。在未开启认证的实例上你mongo直接连进去就是超级权限一旦在配置里加上security.authorization: enabled并重启所有连接都必须提供凭据包括你从本机连的。所以开启认证之前务必先创建一个管理员账号否则重启后你会发现自己被锁在门外——这个坑我见过不止一个人踩。2.2 用户是挂在某个库下的但权限可以跨库MongoDB 的用户是“属于某个数据库”的创建用户时必须指定在哪个库里创建。比如你在admin库里创建的用户和你在appdb库里创建的用户是两个完全独立的实体哪怕用户名一模一样。这一点跟很多关系型数据库的“全局用户”概念不同刚开始接触容易懵。但用户的权限可以跨库。一个在admin库里创建的用户可以被授予对appdb、logdb等多个库的操作权限。这就引出了一个实践中的常见做法把管理类账号统一建在admin库把业务类账号建在各自的业务库。管理账号负责运维、备份、用户管理业务账号只负责自己那摊数据的读写。这样职责清晰出问题也好定位。还有一个特殊库叫local它存放的是副本集相关的本地数据不参与复制。一般不建议在local库里建业务用户它的定位就是节点本地信息。2.3 角色是权限的集合用户通过角色获得权限MongoDB 不直接把权限授予用户而是通过角色Role这个中间层。角色是一组权限的集合用户被授予某个角色就获得了该角色包含的所有权限。这样做的好处是权限可以复用、可以组合、可以自定义。角色分两类内置角色和自定义角色。内置角色是 MongoDB 预先定义好的覆盖了从“只读”到“超级管理员”的各种常见需求自定义角色则是你根据业务需要用createRole命令自己拼出来的。内置角色能满足大部分场景但遇到“只能读某几个集合的某几个字段”这种精细需求就得靠自定义角色。角色还有继承关系。一个角色可以包含另一个角色形成角色树。比如内置的readWrite角色其实就包含了read角色的所有权限再加上写权限。你授予用户readWrite他自然就有了read的能力。理解继承关系能帮你在排查“为什么这个用户能执行某个操作”时快速定位到是哪个角色带来的权限。3. 内置角色全盘点什么场景该给什么角色3.1 数据库级别的内置角色数据库级别的角色作用于某一个具体的库授予时也要指定库名。下面这张表是我整理的最常用的几个基本覆盖了日常开发运维的需求。角色名权限范围典型使用场景read只读该库所有非系统集合报表账号、数据分析只读账号readWrite读写该库所有非系统集合业务应用连接账号dbAdmin该库的索引、集合、统计等管理操作不含数据读写库结构维护账号userAdmin该库的用户和角色管理业务库的用户管理员dbOwner等于readWritedbAdminuserAdmin库的完全负责人这里要特别提醒read和readWrite默认不包含对系统集合如system.users、system.roles的访问权限。也就是说一个只有readWrite的账号是看不到用户列表的。这是有意为之的设计避免业务账号窥探权限配置。如果你确实需要某个账号能查看用户信息得额外授予userAdmin或readAnyDatabase之类的角色。dbAdmin这个角色容易被误用。它管的是库的结构层面——建索引、删集合、看统计信息但不能读写业务数据。有些同学想给运维账号一个“能管理但不能碰数据”的身份dbAdmin就是干这个的。反过来如果你给一个账号只授了dbAdmin然后发现它查不了数据别惊讶这是符合预期的。3.2 集群级别的内置角色集群级别的角色作用于整个实例或副本集授予时在admin库操作。常用的有clusterAdmin副本集配置、分片、节点管理等集群级操作权限很大慎用。clusterMonitor只读的集群监控权限适合监控系统对接。hostManager节点层面的管理如关闭节点、查看日志。backup/restore备份和恢复专用角色backup能读所有数据用于备份restore能写所有数据用于恢复。backup和restore这两个角色值得单独说。它们的存在是为了让备份工具能在不授予完整root权限的情况下完成工作。backup角色能读取所有库的数据包括系统库的部分信息但不能修改restore角色能写入所有库但不能读取。这种“读写分离”的设计是为了降低备份账号被盗用后的风险——一个只能读的账号即使泄露也无法破坏数据。3.3 超级角色root和它的替代方案root角色是 MongoDB 里权限最大的内置角色拥有所有库的所有权限包括用户管理、集群管理、数据读写。很多教程图省事直接让你建一个root账号就完事。但在生产环境我强烈建议不要用root作为日常连接账号。原因很简单root权限太大一旦这个账号的凭据泄露攻击者可以做任何事——删库、改用户、导出全部数据。而且root账号的操作审计也很麻烦你很难从日志里区分哪些是正常运维、哪些是异常行为。更合理的做法是“按需拆分”建一个admin库下的管理账号授予userAdminAnyDatabaseclusterAdminreadWriteAnyDatabase这样的组合用于日常运维再建一个只读的监控账号给监控系统业务账号则各自在自己的库里用readWrite。这样每个账号的权限边界清晰出问题能快速定位也符合最小权限原则。提示userAdminAnyDatabase这个角色只能管理用户和角色不能读写业务数据。它是“用户管理员”而非“数据库管理员”别被名字误导。4. 手把手创建用户从管理员到业务账号的完整流程4.1 开启认证前的准备工作在开启认证之前你得先确认几件事。第一MongoDB 的配置文件路径通常在/etc/mongod.conf或安装目录下的mongod.conf。第二确认当前实例是否已经有用户数据如果之前跑过createUser但没开认证那些用户是存在的开启认证后就能直接用。操作顺序很重要先创建管理员用户再开启认证最后重启服务。如果你先开认证再建用户重启后就连不上了。具体步骤用无认证方式连接到实例此时还没开认证直接连即可。切换到admin库use admin。创建管理员用户授予userAdminAnyDatabase和clusterAdmin等角色。编辑配置文件在security段加上authorization: enabled。重启 MongoDB 服务。用刚创建的管理员账号重新连接验证认证是否生效。这里有个细节创建用户时的pwd字段密码强度要够。MongoDB 本身不强制密码复杂度但生产环境建议至少 16 位包含大小写、数字和符号。别用123456这种SCRAM 虽然不传明文但弱密码依然能被离线爆破。4.2 创建管理员账号的具体命令下面这段是在admin库里创建一个管理账号的示例。用户名和密码都是虚构的你替换成自己的。use admin db.createUser({ user: opsAdmin, pwd: Str0ng_Pssw0rd_2024, roles: [ { role: userAdminAnyDatabase, db: admin }, { role: clusterAdmin, db: admin }, { role: readWriteAnyDatabase, db: admin }, { role: backup, db: admin } ] })这段命令里roles数组的每一项都是一个角色引用role是角色名db是这个角色所属的库。注意readWriteAnyDatabase和backup这类集群级角色它们的db必须写admin因为它们是定义在admin库下的。创建成功后MongoDB 会返回一个{ ok: 1 }之类的响应。你可以用db.getUsers()查看当前库下的所有用户确认新账号已经存在。但注意getUsers需要userAdmin或userAdminAnyDatabase权限如果你当前连接没有这个权限会报错。4.3 业务账号的最小权限实践业务账号的原则是“够用就行”。一个典型的 Web 应用通常只需要对某个业务库的读写权限。假设业务库叫contentdb应用账号叫contentApp创建命令如下use contentdb db.createUser({ user: contentApp, pwd: An0ther_Str0ng_Pwd!, roles: [ { role: readWrite, db: contentdb } ] })这个账号只能读写contentdb不能碰其他库也不能管理用户。如果应用还需要执行一些聚合查询或创建索引readWrite已经包含了这些能力索引创建属于写操作。但如果应用需要执行db.stats()这类统计命令readWrite是不够的得额外加dbAdmin或clusterMonitor。我见过一种情况开发同学为了方便调试给应用账号加了readAnyDatabase想着“反正只是多读几个库”。结果上线后这个账号能读到其他业务库的数据包括一些敏感信息。这就是权限过度的典型后果。调试期的临时权限上线前一定要回收。4.4 用户信息的查看、修改与删除查看用户用db.getUsers()查看某个用户的详细信息用db.getUser(username)。修改用户信息主要涉及改密码、改角色、改自定义数据。改密码用db.changeUserPassword()改角色用db.updateUser()。// 改密码 db.changeUserPassword(contentApp, New_Str0ng_Pwd2024) // 改角色先清空再重新授予 db.updateUser(contentApp, { roles: [ { role: readWrite, db: contentdb }, { role: read, db: logdb } ] })注意updateUser的roles字段是全量替换不是追加。你写什么用户最终就有什么角色。所以改角色之前最好先用getUser把当前角色查出来在原有基础上调整避免误删。删除用户用db.dropUser(username)。删除前确认这个账号没有被任何应用或脚本使用否则会导致连接失败。我一般会在删除前先禁用账号改密码或移除角色观察一段时间确认没有影响再彻底删除。5. 自定义角色当内置角色不够用时怎么办5.1 什么情况下需要自定义角色内置角色虽然多但粒度是“库级别”的。如果你的需求是“只能读orders集合不能读users集合”或者“只能对logs集合执行插入不能更新和删除”内置角色就无能为力了。这时候就需要自定义角色。自定义角色的核心是权限Privilege的定义。一个权限由三部分组成资源Resource和允许的操作Actions。资源描述“对什么操作”可以是某个库、某个集合、甚至某个集群操作描述“能做什么”比如find、insert、update、remove等。5.2 用createRole定义一个精细角色假设有个场景一个日志采集服务只需要向logdb库的app_logs集合插入数据不能查询、不能修改、不能删除。这个需求用内置角色做不到得自定义。use logdb db.createRole({ role: logInserter, privileges: [ { resource: { db: logdb, collection: app_logs }, actions: [insert] } ], roles: [] })这段命令创建了一个叫logInserter的角色它只有一个权限对logdb.app_logs集合执行insert操作。roles字段是空数组表示这个角色不继承任何其他角色。创建好之后把这个角色授予采集账号db.grantRolesToUser(logCollector, [ { role: logInserter, db: logdb } ])这样logCollector就只能往app_logs里插数据别的什么都干不了。即使这个账号泄露攻击者也无法读取已有日志或破坏数据。5.3 资源描述的几种粒度资源描述可以细到集合级别也可以粗到集群级别。常见的写法有{ db: mydb, collection: mycoll }指定库的指定集合。{ db: mydb, collection: }指定库的所有集合空字符串表示所有。{ cluster: true }集群级别资源用于clusterAdmin类操作。{ db: mydb, collection: system.js }系统集合也可以单独指定。操作Actions的取值取决于资源类型。对集合资源常见的有find、insert、update、remove、createIndex、dropIndex等对数据库资源有createCollection、dropCollection、listCollections等对集群资源有shutdown、replSetGetStatus等。完整的操作列表可以在官方文档的“Privilege Actions”页面查到这里不一一列举。5.4 自定义角色的继承与复用自定义角色也可以继承其他角色包括内置角色和其他自定义角色。比如你想定义一个“只读但能看用户列表”的角色可以这样写db.createRole({ role: readWithUserList, privileges: [ { resource: { db: contentdb, collection: system.users }, actions: [find] } ], roles: [ { role: read, db: contentdb } ] })这个角色继承了read的所有权限再加上对system.users集合的find权限。这样用户既能读业务数据又能查看用户列表但依然不能修改用户。继承的好处是复用。如果你有多个角色都需要“读业务数据”这个基础能力可以定义一个基础角色其他角色继承它避免重复定义权限。但继承层数不宜过深一般两三层就够了太深了排查权限来源会很痛苦。6. 权限验证与故障排查认证失败、权限不足怎么查6.1 认证失败的常见原因认证失败通常报Authentication failed。原因可能有几种用户名或密码错误、认证库选错、认证机制不匹配、用户不存在。排查时先确认你连的是哪个库——mongo命令的--authenticationDatabase参数指定了认证库如果不指定默认用连接字符串里的库。很多“密码明明对却连不上”的情况其实是认证库选错了。比如用户在admin库创建但你连接时没指定--authenticationDatabase adminMongoDB 就会去你当前连接的库里找这个用户找不到自然认证失败。正确的连接方式mongo --host 127.0.0.1 --port 27017 \ --username opsAdmin \ --password \ --authenticationDatabase admin--password后面不跟值会提示你交互式输入避免密码出现在命令历史里。这是个好习惯生产环境尤其要注意。6.2 权限不足的排查路径权限不足报not authorized on db to execute command。排查时按这个顺序走确认当前用户是谁db.runCommand({ connectionStatus: 1 })会返回当前认证用户和其角色列表。这是排查权限问题的第一步先搞清楚“我是谁”。确认用户有哪些角色db.getUser(username)查看角色列表注意每个角色属于哪个库。确认角色包含哪些权限db.getRole(roleName, { showPrivileges: true })查看角色的详细权限包括继承来的。确认操作需要的权限对照官方文档看你要执行的操作需要什么权限。对比差异如果用户角色里没有这个权限就是授权不足如果有但依然报错可能是资源描述不匹配比如角色授权的是mydb你操作的是otherdb。这个排查链路我实际用过很多次基本能定位 90% 以上的权限问题。剩下 10% 可能是角色继承的层级太深或者有多个同名角色在不同库下需要更仔细地比对。6.3 一个真实的排查案例之前有个同事反馈说他的账号能读contentdb的数据但执行db.contentdb.stats()时报权限不足。我按上面的路径查了一遍connectionStatus显示他是contentApp角色是readWritecontentdb。getRole看readWrite的权限发现它包含find、insert、update、remove等数据操作但不包含dbStats这个操作。dbStats属于数据库级别的统计操作需要dbAdmin或clusterMonitor角色。问题定位了readWrite管数据不管统计。解决方案是给这个账号额外授予dbAdmincontentdb或者单独定义一个包含dbStats操作的自定义角色。同事最后选了后者因为不想让业务账号有dbAdmin那么大的权限。这个案例说明一个道理内置角色的权限边界是固定的遇到边界外的需求要么换角色要么自定义。别指望一个readWrite走天下。7. 生产环境用户权限管理的几条经验7.1 账号命名要有规律我习惯用“用途环境”的方式命名账号比如app_prod_content、monitor_prod、backup_prod。这样从名字就能看出这个账号是干什么的、用在哪个环境。避免用user1、test这种无意义的名字时间一长根本记不住谁是谁。7.2 密码管理要集中生产环境的数据库密码不应该散落在各个配置文件里。我见过一个项目数据库密码在应用配置、脚本、CI 配置里各存了一份改密码时要改五六个地方漏一个就出故障。更好的做法是用配置中心或密钥管理服务统一管理应用启动时动态获取。MongoDB 本身不支持密码轮换但你可以通过changeUserPassword配合应用的重连机制来实现平滑轮换。7.3 定期审计用户和角色建议每个季度做一次权限审计列出所有用户、他们的角色、最后登录时间如果开启了审计日志检查有没有长期不用的账号、有没有权限过大的账号、有没有角色定义已经过时的自定义角色。这个工作听起来麻烦但比起权限泄露后的损失这点时间投入是值得的。7.4 备份账号和恢复账号要分开前面提到过backup和restore角色它们的设计初衷就是读写分离。实际使用中备份任务用backup账号恢复任务用restore账号不要图省事用一个root全包了。这样即使备份账号的凭据泄露攻击者也只能读不能写破坏力有限。7.5 开启认证之后别忘了审计日志MongoDB 企业版有审计日志功能社区版没有内置的审计但可以通过db.setProfilingLevel开启慢查询日志间接记录一些操作。更完整的做法是在应用层记录数据库操作日志或者用外部工具采集 MongoDB 的日志文件。认证和授权只是安全的第一步没有审计你永远不知道谁在什么时候做了什么。8. 写在最后权限管理是习惯不是一次性任务我刚开始接触 MongoDB 权限的时候也觉得这些东西繁琐——建个账号而已搞这么多角色、继承、资源描述干什么。但经历过几次“账号权限过大导致误操作”和“权限不足排查半天”之后我慢慢理解了这套设计的价值它逼着你去思考“这个账号到底需要什么”而不是“先给个大的再说”。MongoDB 的权限体系不算最复杂的但它的粒度足够细细到你可以精确控制到某个集合的某个操作。用好这套体系关键不在于记住多少命令而在于养成“最小权限”的思维习惯——每次创建账号时多问一句“它真的需要这个权限吗”每次排查问题时多走一遍“我是谁、我有什么角色、角色有什么权限”的链路。这些习惯一旦形成你会发现权限问题不再是玄学而是有迹可循的工程问题。最后分享一个小技巧如果你不确定某个操作需要什么权限可以先用一个权限较大的账号执行然后查看 MongoDB 的日志日志里会记录“哪个用户尝试执行什么操作、被什么权限拒绝”。反过来推就能知道需要补什么权限。这个方法在排查自定义角色的权限缺失时特别管用。