ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

EMQX SCRAM 认证 HTTP API 用户创建返回 user_id 修复:问题、根因与源码级解析

EMQX SCRAM 认证 HTTP API 用户创建返回 user_id 修复:问题、根因与源码级解析 后端物联网消息队列通信【免费下载链接】emqxThe most scalable and reliable MQTT broker for AI, IoT, IIoT and connected vehicles项目地址https://gitcode.com/gh_mirrors/em/emqx点击查看免费下载导读本文围绕 EMQX当前开源仓库中一项针对SCRAM 认证 HTTP API的缺陷修复展开此前调用创建用户接口时接口返回的user_id与实际创建的用户 ID 不一致。文章将以该修复为核心梳理 SCRAM 认证scrambuilt_in_database在 HTTP API 层的完整用户管理调用链结合仓库源码说明缺陷根因、修复后的正确行为并给出可直接复现与验证的 API 用法。读完本文你将掌握 EMQX 内置数据库 SCRAM 认证的创建/查询/更新/删除/轮换密码接口语义以及接口返回值必须与实际写入数据一致这一契约在源码中的实现方式。1. 缺陷描述创建用户接口返回了错误的 user_id本次修复记录位于仓库 changes/ee/fix-16459.en.md原文如下Fixed the issue in SCRAM authentication HTTP API. Previously, incorrect user ID was returned for the created user in the user creation API call.翻译过来即修复了 SCRAM 认证 HTTP API 中的问题——此前调用用户创建 API时返回的 user_id 并非实际创建的那个用户。这条 changelog 虽短但指向明确问题发生在SCRAM 认证认证机制为scram后端为内置数据库built_in_database的用户创建 HTTP 接口上属于典型的API 返回值与后端真实写入状态不一致类缺陷。这类问题在自动化运维场景中危害明显——脚本若依赖创建接口返回的user_id做后续绑定、授权或审计就会拿到错误的标识符。需要说明的是该修复记录位于changes/ee/目录企业版变更记录因此该缺陷及其修复首先出现在 EMQX 企业版渠道但从下文源码分析可以看到相关的用户管理逻辑位于开源仓库中apps/emqx_auth_mnesia与apps/emqx_auth两个应用修复本身可直接在开源代码中定位与验证。2. SCRAM 认证与内置数据库用户管理全景在深入缺陷根因之前先厘清 SCRAM 认证在 EMQX 中的位置与用户管理的整体架构。2.1 SCRAM 认证是什么SCRAMSalted Challenge Response Authentication MechanismRFC 5802是一种不传输明文密码的挑战-响应认证机制。EMQX 内置数据库认证支持两种机制password_based密码散列scramSCRAM 挑战-响应仓库 apps/emqx_auth_mnesia/include/emqx_auth_mnesia.hrl 中定义了机制与后端的常量-define(AUTHN_MECHANISM_SIMPLE, password_based). -define(AUTHN_MECHANISM_SIMPLE_BIN, password_based). -define(AUTHN_MECHANISM_SCRAM, scram). -define(AUTHN_MECHANISM_SCRAM_BIN, scram). -define(AUTHN_BACKEND, built_in_database). -define(AUTHN_BACKEND_BIN, built_in_database).SCRAM 认证的关键特性源码中体现为约束包括密码不以明文落库创建用户时客户端提交的明文密码会被转化为stored_keyStoredKey、server_keyServerKey和salt盐值存储明文密码本身不持久化不支持批量导入正因为盐值和密钥由明文密码推导且明文不落盘SCRAM 认证的导入用户操作被显式拒绝。见 emqx_authn_scram_mnesia.erl%% SCRAM credentials cannot be bulk-imported: salts and stored/server keys %% derive from the plaintext password, which is never persisted. The provider %% advertises user-management support generally, so we surface the limitation %% here rather than letting dispatch crash. import_users(_File, _State) - {error, unsupported_operation}.不支持密码轮换rotate_password接口对 SCRAM 类型返回Operation not supported in this authentication type见下文测试用例。2.2 用户管理 API 的完整调用链EMQX 内置数据库认证的用户管理 API 由 apps/emqx_auth/src/emqx_authn/emqx_authn_api.erl 暴露路由定义于该文件/authentication/:id/users/:user_id 全局认证链用户管理 /authentication/:id/users/:user_id/password/rotate /listeners/:listener_id/authentication/:id/users/:user_id 监听器级认证链调用链如下HTTP API (emqx_authn_api) └─ add_user / update_user / find_user / delete_user / rotate_password └─ emqx_authn_chains:add_user / update_user / ... └─ emqx_authn_scram_mnesia:add_user / do_add_user / do_update_user / ... └─ mria 事务 mnesia 写入 (?TAB / ?NS_TAB)其中用户数据存储层位于 apps/emqx_auth_mnesia/src/emqx_authn_scram_mnesia.erlSCRAM 凭证的生成盐值、StoredKey、ServerKey由sasl_auth_scram:generate_authentication_info/2完成。存储上区分两个表全局命名空间用户#user_info{}记录表?TABuser_id形如{UserGroup, UserId}命名空间多租户用户#?NS_TAB{}记录表?NS_TABuser_id形如?NS_KEY(Namespace, UserGroup, UserId)三元组编码了命名空间、用户组与用户 ID。user_id的编码与解码key/3、rec_to_map/1、format_user_info/1集中在 emqx_authn_scram_mnesia.erlkey(?global_ns, UserGroup, UserId) - {UserGroup, UserId}; key(Namespace, UserGroup, UserId) when is_binary(Namespace) - ?NS_KEY(Namespace, UserGroup, UserId).对外输出时统一取元组中的UserIdformat_user_info(#user_info{user_id {_, UserId}, is_superuser IsSuperuser}) - #{user_id UserId, is_superuser IsSuperuser}; format_user_info(#?NS_TAB{user_id ?NS_KEY(_, _, UserId), is_superuser IsSuperuser}) - #{user_id UserId, is_superuser IsSuperuser}.可以看到对外暴露的用户 ID与存储层复合主键之间存在一层解包逻辑这正是本缺陷容易产生偏差的位置。3. 缺陷根因剖析返回值为何与真实写入不一致3.1 从源码看应当一致的契约先看修复后当前仓库状态的正确行为。HTTP 层创建用户的入口add_user/3位于 emqx_authn_api.erladd_user(ChainName, AuthenticatorID, #{user_id : UserID} RawUserInfo) - Namespace maps:get(namespace, RawUserInfo, ?global_ns), IsSuperuser maps:get(is_superuser, RawUserInfo, false), PasswordField case RawUserInfo of #{password : Password} - #{password Password}; _ - #{} end, UserInfo maps:merge( #{ namespace Namespace, user_id UserID, is_superuser IsSuperuser }, PasswordField ), case check_superuser_allowed(Namespace, IsSuperuser) of ok - case emqx_authn_chains:add_user(ChainName, AuthenticatorID, UserInfo) of {ok, User} - {201, user_out(User)}; {error, Reason} - serialize_error({user_error, Reason}) end; ... end;这里 HTTP 层把请求体中的user_id原样放入UserInfo透传下去然后直接透传后端返回的User并序列化给客户端。因此创建接口返回什么user_id完全取决于认证提供者emqx_authn_scram_mnesia在写入后返回了什么。存储层do_add_user/1emqx_authn_scram_mnesia.erldo_add_user(UserInfoRecord) - case do_lookup_by_rec_txn(UserInfoRecord) of [] - ok insert_user(UserInfoRecord), #{ namespace : Namespace, user_id : UserId, is_superuser : IsSuperuser } rec_to_map(UserInfoRecord), {ok, #{namespace Namespace, user_id UserId, is_superuser IsSuperuser}}; [_] - {error, already_exist} end.即先写入insert_user再把rec_to_map/1解包出的、确实写入表中的UserId作为返回值。rec_to_map/1emqx_authn_scram_mnesia.erl从记录中解包rec_to_map(#user_info{} Rec) - #user_info{ user_id {UserGroup, UserId}, ... } Rec, #{ namespace ?global_ns, user_id UserId, user_group UserGroup, ... }; rec_to_map(#?NS_TAB{} Rec) - #?NS_TAB{ user_id ?NS_KEY(Namespace, UserGroup, UserId), ... } Rec, #{ namespace Namespace, user_id UserId, user_group UserGroup, ... }.同时user_info_record/5emqx_authn_scram_mnesia.erl在构造记录时也把请求中的UserId与UserGroup即认证器 ID编码进复合主键user_info_record(?global_ns, UserGroup, UserId, Password, IsSuperuser, State) - {StoredKey, ServerKey, Salt} sasl_auth_scram:generate_authentication_info(Password, State), #user_info{ user_id {UserGroup, UserId}, stored_key StoredKey, server_key ServerKey, salt Salt, is_superuser IsSuperuser };综合可见请求的user_id→ 复合主键 → 表中记录 →rec_to_map解包 → HTTP 响应这一条链在修复后是严格闭环一致的。修复前的缺陷正是这条链上出现了偏差——返回给客户端的user_id取自了错误的字段或错误的位置例如直接取复合键的某一错误分量、或使用了占位/默认值导致返回的 user_id 不是实际创建的用户。修复的本质就是让返回值回到以实际写入记录的复合主键解包结果为准这一契约上。3.2 为什么是 SCRAM 特有与其他机制的差异该问题被标记为SCRAM 认证 HTTP API特有从源码结构看可以推断其背景是SCRAM 提供者emqx_authn_scram_mnesia与普通密码散列提供者emqx_authn_mnesia共用同一套 HTTP 用户管理入口但各自维护独立的存储记录格式与转换函数rec_to_map、format_user_info。任何一边在返回用户信息时若未严格复用写入后解包的结果就会出现与其他机制行为不一致的返回内容。SCRAM 这边记录结构含stored_key/server_key/salt等额外字段格式转换路径更长出现偏差的概率也更高。3.3 测试用例对修复行为的固化仓库测试套件 apps/emqx_auth_mnesia/test/emqx_authn_api_mnesia_SUITE.erl 中固化了创建接口返回真实 user_id的断言例如创建后校验返回体lists:foreach( fun(User) - {201, CreatedUser} add_user(User), ?assertMatch( #{ user_id : _, namespace : NsAPIOut }, CreatedUser, #{expected_ns NsAPIOut} ) end, ValidUsers ),以及用户存在性检查后抓取返回的user_id{201, _} add_user(User), ... ?assertMatch(#{user_id : u1}, FetchedUser),此外t_scram_user_api_errors/1emqx_authn_api_mnesia_SUITE.erl专门针对 SCRAM 用户 API 的边界行为做了断言可作为理解该 API 语义的参考t_scram_user_api_errors(_TCConfig) - put_auth_header(create_superuser()), ScramConfig #{ mechanism scram, backend built_in_database, algorithm sha512, iteration_count 4096 }, {200, _} create_authenticator(ScramConfig), AuthenticatorID scram:built_in_database, {400, #{message : PasswordRequired}} add_authenticator_user(AuthenticatorID, #{user_id scram-user}), ?assertNotEqual(nomatch, binary:match(PasswordRequired, Password is required)), {201, _} add_authenticator_user(AuthenticatorID, #{ user_id scram-user, password scram-password }), {400, #{message : Unsupported}} rotate_authenticator_password(AuthenticatorID, scram-user), ?assertEqual(Operation not supported in this authentication type, Unsupported), ok.该用例同时印证了三件事SCRAM 创建用户缺少密码时返回 400Password is required——因为密钥必须由明文密码生成正确提交user_idpassword时返回 201SCRAM不支持密码轮换rotate_password返回 400。4. 完整用户管理 API 实操请求与响应语义以下 API 均基于修复后的当前仓库行为。认证链用户管理接口全局认证链路径前缀为/api/v5/authentication/:id/users监听器级接口为/api/v5/listeners/:listener_id/authentication/:id/users:id为认证器 ID如scram:built_in_database。4.1 创建用户POSTPOST /api/v5/authentication/:id/users请求体{ user_id: scram-user, password: scram-password, is_superuser: false }可选字段字段类型默认值说明user_idstring必填用户名/用户标识缺失时返回 400missing_parameter: user_id见 emqx_authn_api.erlpasswordstring必填SCRAM明文密码SCRAM 机制缺失时返回 400Password is requiredis_superuserbooleanfalse是否超级用户namespacestring全局命名空间仅限全局管理员为多租户命名空间创建用户时使用成功响应201{ user_id: scram-user, is_superuser: false, namespace: null }注意全局命名空间下返回体中namespace为null见 emqx_authn_api.erl 的user_out/1User#{namespace : null}命名空间用户则返回实际命名空间名。user_id必须与请求体中的user_id一致——这正是本次修复保证的契约。重复创建同一用户返回 409already_exist。4.2 查询用户列表GETGET /api/v5/authentication/:id/users支持分页与模糊查询参数其中like_user_id用于按用户名子串模糊过滤emqx_authn_api.erl多命名空间场景可用ns查询参数指定命名空间。模糊匹配的底层实现见 emqx_authn_scram_mnesia.erl通过binary:match实现子串匹配。4.3 查询/更新/删除单个用户GET /api/v5/authentication/:id/users/:user_id PUT /api/v5/authentication/:id/users/:user_id DELETE /api/v5/authentication/:id/users/:user_idGET按user_id精确查找返回{user_id, is_superuser, namespace}不存在时返回 404not_foundPUT可更新password重新生成盐值与 StoredKey/ServerKey与is_superuser至少需提供其一否则返回 400missing_parameter: password见 emqx_authn_api.erl更新成功后同样返回格式化后的完整用户信息DELETE删除指定用户成功返回 204用户不存在返回 404。更新操作的字段筛选见fields_to_update/3emqx_authn_scram_mnesia.erl仅keys_and_salt由密码推导与is_superuser两个字段会被实际写回。4.4 密码轮换SCRAM 不支持POST /api/v5/authentication/:id/users/:user_id/password/rotate对 SCRAM 认证器调用返回 400{ message: Operation not supported in this authentication type }这是 SCRAM 的固有约束盐值与密钥随密码生成并落库服务端无从进行类似 JWT 那样的无状态密码轮换。4.5 命名空间多租户语义对于启用了命名空间Namespace的部署用户管理 API 支持两种指定命名空间的方式emqx_authn_api.erl请求体中的namespace字段查询参数ns。两者的解析与鉴权遵循以下规则未指定时使用调用者管理员账号自身的命名空间全局管理员?global_ns可以操作任意命名空间普通命名空间管理员只能操作自身命名空间越权操作返回 403。测试用例t_delete_user_namespace_resolution/1emqx_authn_api_mnesia_SUITE.erl验证了 body 与 query 两种方式均可独立选择删除目标命名空间。多命名空间下的写入存储于独立的?NS_TAB表复合主键为?NS_KEY(Namespace, UserGroup, UserId)。5. SCRAM 认证器的配置参数SCRAM 内置数据库认证器的配置 schema 位于 apps/emqx_auth_mnesia/src/emqx_authn_scram_mnesia_schema.erlfields(scram) - [ {mechanism, emqx_authn_schema:mechanism(?AUTHN_MECHANISM_SCRAM)}, {backend, emqx_authn_schema:backend(?AUTHN_BACKEND)}, {algorithm, fun algorithm/1}, {iteration_count, fun iteration_count/1} ] emqx_authn_schema:common_fields().配置项类型/取值默认值说明mechanismscram—认证机制固定为 SCRAMbackendbuilt_in_database—后端存储固定为内置数据库algorithmsha256/sha512sha256散列算法schema 定义iteration_count非负整数4096SCRAM 迭代次数schema 定义越大越安全但认证开销越高enablebooleantrue认证器启用开关common fieldsuser_id_typeusername/clientidusername用户 ID 取自 MQTT 连接的哪个字段common fields认证器创建后其user_group即认证器 ID如scram:built_in_database创建用户的create/2回调把它作为用户记录的user_group分量emqx_authn_scram_mnesia.erl这也是复合主键{UserGroup, UserId}的来源。测试中使用的配置示例algorithm sha512, iteration_count 4096与 schema 默认值保持一致。6. 总结与验证要点本次修复的核心契约可以归纳为一句话创建用户 API 返回的user_id必须与实际写入内置数据库记录中的用户 ID 一致。验证修复是否生效可以从三个层面入手API 层POST /api/v5/authentication/:id/users创建 SCRAM 用户后断言响应体201中的user_id与请求体中的user_id完全一致参考测试 emqx_authn_api_mnesia_SUITE.erl数据层通过GET /api/v5/authentication/:id/users/:user_id查询同一用户确认返回的user_id与创建时的请求/响应一致参考 emqx_authn_api_mnesia_SUITE.erl源码层确认emqx_authn_scram_mnesia的do_add_user/1返回值取自rec_to_map/1解包出的复合主键UserId而非请求体之外的其他来源。对该缺陷的修复提醒我们在认证这类对数据一致性高度敏感的场景中写什么就返回什么必须由同一份数据源驱动任何在返回路径上另起炉灶的字段处理都可能制造出 API 契约与真实状态脱节的隐性缺陷。EMQX 通过让返回体直接复用写入后解包的记录从代码结构上杜绝了这类偏差这也是本次修复最值得借鉴的实现思路。涉及的核心文件索引修复记录changes/ee/fix-16459.en.mdHTTP API 层apps/emqx_auth/src/emqx_authn/emqx_authn_api.erlSCRAM 存储与凭证逻辑apps/emqx_auth_mnesia/src/emqx_authn_scram_mnesia.erlSCRAM 配置 schemaapps/emqx_auth_mnesia/src/emqx_authn_scram_mnesia_schema.erl类型常量定义apps/emqx_auth_mnesia/include/emqx_auth_mnesia.hrl用户管理 API 测试apps/emqx_auth_mnesia/test/emqx_authn_api_mnesia_SUITE.erl赞分享后端物联网消息队列通信【免费下载链接】emqxThe most scalable and reliable MQTT broker for AI, IoT, IIoT and connected vehicles项目地址https://gitcode.com/gh_mirrors/em/emqx点击查看免费下载相关推荐EMQX 连接 MongoDB 8.0 认证失败问题修复解析buildInfo 探测与 SCRAM 认证机制EMQX 连接 MongoDB 8.0 认证失败问题修复解析buildInfo 探测与 SCRAM 认证机制 导读 本文基于 EMQX 仓库中的变更记录 c后端物联网消息队列通信EMQX 集群加入期间监控指标 API 返回 500 的根因分析与修复解读EMQX 集群加入期间监控指标 API 返回 500 的根因分析与修复解读 本篇文章基于 EMQX 开源仓库变更记录 changes/ee/fix 18114.后端物联网消息队列通信EMQX 修复 SCRAM 认证指标计数异常Total 重复累加与 Success 缺失问题剖析EMQX 修复 SCRAM 认证指标计数异常Total 重复累加与 Success 缺失问题剖析 导读 本文围绕 EMQX 开源仓库中 changes/ee/后端物联网消息队列通信创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表