
MongoDB的访问控制很多人一到生产环境就卡壳。我见过不少项目开发环境里不启用认证所有人用同一个连接串连库等上了生产加了--auth参数后一堆报错然后开始急急忙忙地建用户、授权结果要么是权限给大了要么是权限给错了修修补补还留下隐患。其实MongoDB的权限模型并不复杂核心就是两件事认证你是谁和授权你能干什么。而在授权里面最重要的两个粒度就是数据库级别和集合级别。这篇文章我从实际运维的角度把这个话题讲透包括内置角色、自定义角色的原理、它们之间的生效规则以及我踩过的坑。1. 访问控制的两层门先认证再授权很多人对MongoDB的访问控制理解停留在建一个用户名和密码就完事了但这个思路会埋雷。MongoDB启用访问控制--auth或配置文件中security.authorization: enabled之后连接过程中实际上有两道关卡。第一关是认证Authentication。客户端提交用户名、密码、认证库authentication databaseMongoDB去admin库或用户指定的库里查这个用户验证密码的哈希值。默认机制是SCRAM-SHA-256。如果密码不对直接拒绝连接日志里会出现Authentication failed之类的记录。第二关是授权Authorization。认证通过之后MongoDB要回答一个问题这个用户能对哪些数据库、哪些集合、执行哪些操作这个回答完全基于RBAC基于角色的访问控制模型。RBAC模型有三个要素用户User连接时绑定的身份角色Role权限的集合可以内置也可以自定义权限Privilege由资源Resource 操作Action组成权限的结构是类似这样的{ resource: { db: appdb, collection: }, actions: [find, insert, update, remove] }其中collection: 表示该数据库下的所有集合。如果把db和collection具体化比如db: appdb, collection: orders那这个权限就只作用于appdb.orders这个集合。所以数据库级别和集合级别的访问控制本质区别就是权限定义中的资源范围不一样。一个绑在库一个绑在集合。提示认证库和应用数据库是两个概念别混为一谈。用户通常创建在admin库里但授权时可以被赋予任意库的角色。后面我会详细说这个。2. 数据库级别的访问控制最常用的默认粒度2.1 内置角色一览MongoDB内置了很多角色它们绝大多数是数据库级别的也就是说角色一旦被授予作用范围就是指定的整个数据库。最常碰到的有这么几个角色主要权限典型用途read查询所有集合的数据看所有集合的索引信息只读报表账号、备份账号readWrite在read基础上增加插入、更新、删除、创建/删除索引应用主连接账号dbAdmin管理数据库结构比如建集合、管理索引、执行collMod但不能读写数据运维人员做结构变更但不碰业务数据userAdmin管理当前数据库的用户和角色创建、删除、赋权业务库管理员专门管账号dbOwner等同于readWritedbAdminuserAdmin的合并数据库超级管理员可以这样理解readWrite是员工权限dbAdmin是结构管理员权限userAdmin是人事权限dbOwner是三者合一的老板权限。2.2 数据库级别权限的实际影响范围如果你给某个用户授予了appdb上的readWrite角色那么这个用户可以读取和修改appdb下所有集合包括你不想让他碰的那些业务表。这是我反复强调的一点数据库级别的授权无法区分同一个库里的不同集合。举个真实的例子某个系统用了一个库存放业务数据同时存放了操作日志。运维同学图省事给一个外部协作方的账号授予了readWrite角色结果对方不仅能读业务表还能清掉日志表的数据。虽然不是恶意操作但权限范围确实超出了预期。如果你遇到这种需求同一个库内不同的集合需要不同的权限控制——那就要用到集合级别了。2.3 库级别授权的最优实践我建议把数据库级别当作粗粒度默认策略来用一个应用对应一个独立的数据库就给它建一个独立的用户授予该库的readWrite。备份账号授予目标数据库的read如果涉及多个库可以用readAnyDatabase在admin库上授予作用于所有库。需要做DDL建索引、改集合结构的同学额外加上dbAdmin。但这里有一个关键点应用账号除了readWrite尽量不要给dbAdmin或userAdmin。原因很简单一旦应用被注入或者代码被恶意篡改攻击者手里有了dbAdmin权限就可以删索引、可以执行drop集合——破坏力比单纯增删改数据大得多。我自己见过一个事故就是因为应用连接串用的账号挂了dbOwner权限代码里一段异常逻辑把整个集合删了直接导致恢复耗时数小时。如果能用只读账号做的操作绝不用读写账号做。3. 集合级别的访问控制精细到每一张表的权限3.1 为什么需要集合级别数据库级别的权限太粗当多个业务模块共用一个数据库时问题就暴露了。比如一个电商系统users集合存用户资料orders集合存订单logs集合存审计日志。如果只有一个库级readWrite账号所有的微服务都共享同一个账号等于任何一个服务的漏洞都可能导致全库数据被改。集合级别的作用就是把这个范围缩小到某几个集合甚至某一个集合。比如订单服务只需要读写orders集合不需要碰users。审计模块只需要插入logs集合连查询都不给。报表模块只能读orders和products不能读写其他。3.2 集合级别的实现方式自定义角色MongoDB的内置角色没有提供直接集合级的角色只有read、readWrite这种库级的以及针对anyCollection的全库变体所以集合级别权限的核心手段就是自定义角色Custom Roles。创建自定义角色用db.createRole核心就是指定privileges数组里面每一项包含resource和actions。资源定义的几种方式// 指定单个集合 { db: appdb, collection: orders } // 指定一个库的全部集合 { db: appdb, collection: } // 指定所有库的所有集合 { db: , collection: } // 使用普通字符串明确集合名特殊集合名如带点的用引号 { db: appdb, collection: system.profile }操作权限actions常用的有这么几个find查询文档insert插入文档update更新文档remove删除文档createCollection创建集合createIndex/dropIndex创建/删除索引collStats查看集合统计信息dropCollection删除集合listCollections列出集合这个其实默认在库级别会有一些隐式权限但集合级自定义时需要显式包含一个典型的集合级自定义角色db.getSiblingDB(appdb).runCommand({ createRole: orders_write_only, privileges: [ { resource: { db: appdb, collection: orders }, actions: [find, insert, update, remove] } ], roles: [] })roles: []表示该角色不继承其他角色。如果希望这个自定义角色还附带读取其他集合的权限可以在roles里引用内置角色但要注意引用的内置角色范围是库级别作用到整个库相当于把你缩小集合的努力给冲淡了。所以自定义集合级权限时继承要谨慎。3.3 实际场景隔离不同业务的权限我之前在一个项目里就是这么做的。数据库shopdb里有products、inventory、orders、customers、audit_logs五张表。我们分了这样几个账号账号角色权限范围product-svcproduct_rw自定义products和inventory集合可读写order-svcorder_rw自定义orders集合可读写products集合只读audit-writeraudit_insert自定义audit_logs集合只能插入不能查、不能改report-readerreport_read自定义上述业务集合全部只读通过库级read实现这样即使某一个服务被入侵或存在代码漏洞受到的破坏范围也被限制在一两个集合之内。3.4 集合级授权注意的细节在集合级授权里有几个坑我栽过跟头提醒一下更新操作被拆成了两个动作。update这个action看起来简单但在MongoDB内部一条更新操作要求用户同时具备update和insert两个权限。因为upsert更新或插入可能插入新文档所以update权限只覆盖更新已有文档的场景如果你给的角色只包含update应用执行updateOne时会报not authorized。所以要支持完整的更新语义请把insert也加上。删除集合是最危险的动作dropCollection只在集合本身上定义。也就是说即使有dbAdmin权限也不代表能删某个集合除非有对应的dropCollection权限。和数据库级别不同集合级权限对listCollections这类库级元数据操作的感知比较微妙。有时候你会遇到用户能读写某个集合但用代码驱动比如一些ORM时会先去listCollections判断集合是否存在结果没有权限报了Unauthorized。解决办法是在自定义角色里额外加上listCollectionsaction资源范围指向该库的所有集合。所以集合级权限虽然精细但配置时要多留一个心眼把隐式依赖的权限一起考虑进去。4. 生效规则库级和集合级是怎么叠加的4.1 权限取并集不取交集一个用户可以被授予多个角色这些角色可能既有库级的也有集合级的。MongoDB在计算最终权限时采用的是并集规则只要任何一个角色授予了该权限这个用户就拥有该权限。举个例子用户A被授予了appdb的read角色又被授予了appdb.orders集合的readWrite自定义角色。那么用户A最终能读整个appdb同时能写appdb.orders。这个权限范围是叠加的不会因为有了集合级权限就反向限制了库级权限。4.2 更具体的资源定义拥有更高的优先级虽然最终权限是并集但在同一个动作的权限判定上资源匹配是有顺序的。实际判定规则大致是先明确请求的操作和资源在用户的所有权限中查找Action是否匹配如果Action匹配再看Resource是否是目标资源的祖先比如集合属于库、库属于全局也就是说如果用户同时有一个针对appdb所有集合的find权限和一个针对appdb.users集合的find权限当访问appdb.users时两者都能满足规则仍然是一并生效不会互相覆盖。真正的区别体现在你不知道哪个角色到底在起作用时。所以这就要求我们给用户授权的时候脑子里要有一条清晰的最小权限边界。不要以为定义了集合级角色后之前给过的库级readWrite就作废了——它还在。4.3 几个特殊数据库和角色admin库内置几乎所有Any角色readAnyDatabase、readWriteAnyDatabase、dbAdminAnyDatabase、userAdminAnyDatabase、root。root角色是一个超级角色它同时拥有dbOwner、userAdminAnyDatabase、dbAdminAnyDatabase、readWriteAnyDatabase等角色的全部权限一般只用于初始化的管理员账号。local库存储副本集和oplog信息普通用户一般无法给local库授权或者访问它。local不在常规权限授权范围内。config库分片集群的元数据库涉及分片集群的运维操作需要额外的特殊权限。system集合像system.users、system.roles这类集合有着特殊的保护。就算你拥有readWrite也不能直接修改system.users来创建用户——创建用户必须用db.createUser走的是内部门面不能当普通集合插入文档。5. 从零到一实操配置演示我下面用一个完整的案例把整个过程走一遍基于MongoDB 6.0/7.0环境是Ubuntu MongoDB社区版配置文件使用默认的/etc/mongod.conf。5.1 启用认证修改配置文件/etc/mongod.conf加上security: authorization: enabled然后重启服务sudo systemctl restart mongod注意启用认证之前先确保已经有一个具有管理员权限的用户否则你重启之后连不上数据库了。很多人第一次配置时忘了先建管理用户结果锁定在门外需要在noauth模式下重启一次再补救。5.2 创建管理员用户先临时用mongosh以本地回环方式连接如果启用认证后没管理用户可以临时在配置文件中换成authorization: disabled或者用mongosh --port 27017在没有认证模式下进入但生产上一般直接用enableLocalhostAuthBypass临时方案)。mongosh mongodb://127.0.0.1:27017/admin然后创建管理员db.createUser({ user: admin, pwd: your-strong-password, roles: [ { role: root, db: admin } ] })这个admin账号仅在初始化时使用不要放进应用配置里。5.3 创建业务数据库和应用账号切到业务库MongoDB的库是隐式创建的用use命令切换即可use appdb db.createUser({ user: app_user, pwd: app-password-123, roles: [ { role: readWrite, db: appdb } ] })这样app_user对appdb下的所有集合都有读写权限这就是最常规的数据库级别授权。5.4 创建集合级别的自定义角色假设appdb里有三个集合users、orders、logs。现在需要给一个专门写日志的进程建一个只能插入logs集合的账号use appdb db.createRole({ role: log_writer, privileges: [ { resource: { db: appdb, collection: logs }, actions: [insert] } ], roles: [] }) db.createUser({ user: log_writer_user, pwd: log-secret, roles: [ { role: log_writer, db: appdb } ] })另一个只读orders集合但不碰users的报表账号use appdb db.createRole({ role: order_reader, privileges: [ { resource: { db: appdb, collection: orders }, actions: [find, listCollections] } ], roles: [] }) db.createUser({ user: report_user, pwd: report-secret, roles: [ { role: order_reader, db: appdb } ] })为什么order_reader里要加listCollections因为报表程序在启动时会先校验表结构如果连集合都看不到某些驱动会直接报错。这是我加完又补上的一次教训。5.5 验证权限用log_writer_user连接mongosh mongodb://127.0.0.1:27017/appdb --username log_writer_user --password log-secret在mongosh里测试// 插入成功 db.logs.insertOne({ msg: hello logger, ts: new Date() }) // 查询失败因为只有insert权限 db.logs.find() // MongoDBServerError: not authorized on appdb to execute command { find: logs }然后测试对users集合的操作// 插入失败因为资源只匹配logs集合 db.users.insertOne({ name: test }) // MongoDBServerError: not authorized on appdb to execute command { insert: users }这就是集合级别控制带来的边界。6. 常见问题与排查技巧实录6.1 为什么明明给了权限还是报 not authorized最典型的原因有两个。第一用户连接时指定的认证库不对。如果用户在admin库里创建的连接时必须用--authenticationDatabase admin指定认证库否则MongoDB默认用你连接的库作为认证库找不到该账号自然授权失败。第二角色虽然存在但你没有重新登录。在mongosh里给用户新增角色后当前会话需要重连才能刷新权限因为在MongoDB中权限是基于连接会话的新的授权不会自动推送给你当前活跃的连接。6.2 字段级权限为什么做不了MongoDB的RBAC模型粒度是集合级别它没有内置字段级别的权限控制。你没办法定义一个角色只能看email字段不能看password字段这不是MongoDB本身支持的。如果你有这种需求方案是建立视图View在视图定义里用$project或$match把敏感字段过滤掉然后给用户授予视图的读取权限。应用层自己做字段过滤。使用企业版的字段级加密Field Level Encryption。我通常建议第一种副作用最小代码不用动。6.3dbAdmin能读写数据吗不能。dbAdmin是管理数据库结构的角色比如创建集合、管理索引、执行collMod、查看统计信息。但是它对业务集合的文档数据没有查询和更改权限。如果你发现某个账号能建索引、能改表结构但主应用连查询都报权限不足先看看是不是只给了dbAdmin。6.4 备份账号应该用哪一级权限如果只是针对单个库做mongodump给该库的read角色就够了。如果要对整个MongoDB实例做全量备份所有库那需要readAnyDatabase以及一些特殊集合的权限比如backup和restore角色它们是内置的主要用于备份恢复场景。尤其是备份system.users这些授权集合时普通readAnyDatabase是不够的需要显式授权对local和config库的读取或者直接用backup角色。6.5 如何查看一个用户到底有哪些权限用db.getUser()可以看用户基本信息但角色里展开的inheritedPrivileges继承权限不是直接显示的需要更深入一步db.getUser(app_user).roles用这个可以查看该用户被授予的角色。如果你想知道某个用户实际拥有的全部权限包括继承和自定义角色的展开可以切换到用户认证库查db.runCommand({ usersInfo: { user: app_user, db: appdb }, showPrivileges: true })在输出里找到inheritedPrivileges字段就能看到资源操作的完整清单。这个命令经常被我用来排查用户到底能不能删集合之类的争议。6.6 权限设计的一条经验法则我后来总结出一条经验默认给应用账号库级readWrite当同一个库内有多个团队/多个服务接入且集合间相互独立时才切换为集合级自定义角色。不要一开始就追求最精细权限那样配置成本高出问题也难排查。先粗后细逐步收敛权限才符合实际运营节奏。还有一点给我留下很深印象MongoDB启用了认证但没启用authorization的情况通过keyFile等方式之外连接时其实你只是在认证身份授权层面如果没配好很多命令照样被拒。所以每次配置完权限一定要用实际账号去跑一遍完整的读写、建索引、删库等场景别指望一次性成功。我自己做权限变更后的惯例是把变更前用户权限用usersInfo导出一份变更后导出一份做一次diff确信没有意外扩大范围。这习惯救过我几次尤其是多人协作的时候。