bigywater.tech
MongoDB · 85

SCRAM 认证:握手几步,角色权限怎么生效

MongoDB 的 SCRAM 认证握手到底几步、传了什么?服务端存了什么?授权不对时报什么错?这篇手写一遍握手来验证。

2 条
skipEmptyExchange 下 SCRAM-SHA-256 握手需要的命令数(否则 3 条)
18
密码错和用户不存在都是同一个错误码 AuthenticationFailed
15000
SCRAM-SHA-256 的 iterationCount(SHA-1 是 10000)

未认证与 localhost 例外

实测 · 本机执行输出
  find 被拒: code 13 Unauthorized: not authorized on test to execute command { find: "t", filter: {}, lsid: { id: UUID("fb8f3d31-98b5-464b-ab2e-1624a4c475ed") }, $db: "test" }, full err
  listCollections 被拒: code 13 Unauthorized: not authorized on test to execute command { listCollections: 1, cursor: {}, nameOnly: true, lsid: { id: UUID("fb8f3d31-98b5-464b-ab2e-1624a4c475ed") }
  不需要认证的命令:ping → {'ok': 1.0};hello 里 saslSupportedMechs 需要先给 user 参数 → ['SCRAM-SHA-256', 'SCRAM-SHA-1']
  localhost 例外:第一个用户可以不认证就创建: {'ok': 1.0}
  创建第一个用户之后,再创建第二个用户(没认证): code 13 Unauthorized: Command createUser requires authentication, full error: {'ok': 0.0, 'errmsg': 'Command createUser requires authentication', 'code': 13, 'codeName': 'U

--auth 下未认证的 find 是 code 13;还没有用户时本机连接可以创建第一个用户,此后创建用户也要认证。

system.users

实测 · 本机执行输出
  顶层字段: ['_id', 'credentials', 'db', 'roles', 'user', 'userId'],_id = admin.alice,mechanisms = ['SCRAM-SHA-1', 'SCRAM-SHA-256']
  SCRAM-SHA-1: iterationCount = 10000,salt 长度 = 16 字节,storedKey 长度 = 20 字节,serverKey 长度 = 20 字节
  SCRAM-SHA-256: iterationCount = 15000,salt 长度 = 28 字节,storedKey 长度 = 32 字节,serverKey 长度 = 32 字节
  roles = [{'role': 'read', 'db': 'test'}]
  明文 "alicepw" 在文档里出现吗? False

用户文档里只有每种机制的 salt、iterationCount、storedKey、serverKey,没有明文密码。

手写握手

实测 · 本机执行输出
  1. 客户端 → 服务器 saslStart:payload = "n,,n=alice,r=LC112qv6JyTl7Cq5l3TfRtv7"(mechanism = SCRAM-SHA-256)
     服务器 → 客户端:conversationId = 1,done = False,payload = "r=<客户端nonce+服务端nonce, 56 字符>,s=30JbBf13…,i=15000"
  2. 客户端 → 服务器 saslContinue:payload = "c=biws,r=…,p=<ClientProof 32 字节的 base64>"
     服务器 → 客户端:done = True,payload = "v=<ServerSignature>"
  自己算出的 storedKey == system.users 里的 storedKey: True;serverKey 相等: True;客户端校验服务器签名通过: True;握手共 2 条命令
  认证后 connectionStatus: [{"user": "alice", "db": "admin"}]
  不带 skipEmptyExchange:握手共 3 条命令(多一次空的 saslContinue)

saslStart 发 n,,n=user,r=nonce,服务器回 salt 和 iterationCount;saslContinue 发 ClientProof,服务器回 ServerSignature。我们自己算出的 storedKey 与服务端一致。

失败的认证

实测 · 本机执行输出
  密码错的 alice: code 18 AuthenticationFailed: Authentication failed., full error: {'ok': 0.0, 'errmsg': 'Authentication failed.', 'code': 18, 'codeName': 'AuthenticationFailed'}
  不存在的用户 nobody: code 18 AuthenticationFailed: Authentication failed., full error: {'ok': 0.0, 'errmsg': 'Authentication failed.', 'code': 18, 'codeName': 'AuthenticationFailed'}
  alice 在错误的 authSource(test): code 18 AuthenticationFailed: Authentication failed., full error: {'ok': 0.0, 'errmsg': 'Authentication failed.', 'code': 18, 'codeName': 'AuthenticationFailed'}
  正确的 alice: {'ok': 1.0}
  机制 PLAIN(服务端没开): code 334 MechanismUnavailable: Received authentication for mechanism PLAIN which is not enabled, full error: {'ok': 0.0, 'errmsg': 'Received authentication for mechanism PLAIN which

密码错、用户不存在、authSource 不对都是同样的 code 18 Authentication failed.。

授权

实测 · 本机执行输出
  alice 读 test.t: []
  alice 写 test.t: code 13 Unauthorized: not authorized on test to execute command { insert: "t", ordered: true, lsid: { id: UUID("ba75afdc-28c3-4e80-9548-026438f0a330") }, $db: "test" }, ful
  alice 读 test2.t(没有授权的库): code 13 Unauthorized: not authorized on test2 to execute command { find: "t", filter: {}, lsid: { id: UUID("ba75afdc-28c3-4e80-9548-026438f0a330") }, $db: "test2" }, full e
  connectionStatus 的 authenticatedUserRoles: [{"role": "read", "db": "test"}]
  root 给 alice 加 readWrite@test 后,alice 的同一个连接写 test.t: 1
  root 回收 readWrite@test 后,同一连接再写: code 13 Unauthorized: not authorized on test to execute command { insert: "t", ordered: true, lsid: { id: UUID("ba75afdc-28c3-4e80-9548-026438f0a330") }, $db: "test" }, ful

只有 read 的用户写入是 code 13;grant/revoke 对同一个已认证连接立即生效。

代价

实测 · 本机执行输出
  不认证的 ping:1.1 ms;SCRAM-SHA-1:2.5 ms;SCRAM-SHA-256:3.4 ms
  (每次都是新建 MongoClient 加一次 ping;客户端和服务端在同一台机器上,没有网络延迟)

新建连接并认证:不认证 1.1 ms,SCRAM-SHA-1 2.5 ms,SCRAM-SHA-256 3.4 ms。

分步演示

SCRAM 认证握手
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / MongoDB 9.0.2(Homebrew 的 mongod、mongos)/ mongosh 2.12.0 / pymongo 4.18.3;每个实验在临时目录里起全新的 mongod(独立 dbpath,端口 9900 以上,只监听 127.0.0.1,WiredTiger 缓存 0.25 GB),需要副本集或分片集群时在本机起多个进程;命令输出来自真实的 mongosh,批量写入、计时与多线程用 pymongo;计时用 time.perf_counter,数字每次运行会有小幅抖动,ObjectId、时间戳等每次运行都不同。单机 mongod --auth(无 keyfile / TLS),pymongo 与手写 saslStart/saslContinue
    • 没有验证:x.509、LDAP、Kerberos 等其它机制;副本集内部认证(keyFile / clusterAuthMode);TLS 对认证的影响;密码长度与 SASLprep 对非 ASCII 密码的处理;authenticationMechanisms 参数的限制;用户缓存(userCacheInvalidationIntervalSecs)在分片集群上的传播延迟;角色变更在分片集群或其它节点上的生效时间(这里只测了单机)
    京ICP备2026067502号-1京公网安备11010502063487号