EMQX 客户端属性初始化:为 `mqtt.client_attrs_init` 表达式新增 `cert_common_name` 与 `cert_subject` 证书变量别名
2026/9/24 2:30:16 网站建设 项目流程
  • 后端
  • 物联网
  • 消息队列
  • 通信

【免费下载链接】emqx

The most scalable and reliable MQTT broker for AI, IoT, IIoT and connected vehicles

项目地址:https://gitcode.com/gh_mirrors/em/emqx
点击查看免费下载

mqtt.client_attrs_init是 EMQX 在客户端连接阶段用表达式初始化客户端属性(Client Attributes)的机制,这些属性随后可用于鉴权 ACL、规则引擎等场景。本变更(见 changes/ee/fix-16865.en.md)为其表达式引入了两个证书相关的变量别名:cert_common_name(等价于原有cn)与cert_subject(等价于原有dn),使 TLS 客户端证书信息在属性初始化表达式中可读性更强、语义更明确。读完本文,你将掌握client_attrs_init的配置结构、证书变量的取值来源,以及如何在配置与 ACL 规则中实际使用这两个新别名。

变更概述:一次清晰的别名增强

本次变更的内容非常聚焦,原文只有一句话:

Addedcert_common_nameandcert_subjectaliases formqtt.client_attrs_initexpressions, alongside the existingcnanddnvariables.

即:在mqtt.client_attrs_init表达式中,新增cert_common_namecert_subject两个变量,作为已有变量cndn的别名,二者可以互换使用。别名引入后,配置语义不再依赖两个含义模糊的缩写,而是直接表达“证书通用名”与“证书主题”,降低了配置的可读性与理解成本。

client_attrs_init是什么:连接期属性初始化机制

在 EMQX 中,客户端属性(client attributes)是挂在clientinfo上的一组键值数据,可被后续鉴权、ACL、消息路由等模块引用。mqtt.client_attrs_init提供了一种声明式手段:在客户端建立连接(CONNECT 处理阶段)时,按配置的表达式列表计算属性并写入客户端信息。

其配置结构定义在 emqx_schema.erl:

fields("client_attrs_init") -> [ {expression, emqx_variform:sc(#{desc => ?DESC("client_attrs_init_expression")})}, {set_as_attr, sc(binary(), #{ desc => ?DESC("client_attrs_init_set_as_attr"), validator => fun restricted_string/1 })} ];

也就是说,client_attrs_init是一个配置项列表,列表中的每一项包含两个字段:

字段类型说明
expressionvariform 表达式计算属性值的表达式,可引用连接阶段的各类变量并调用字符串处理函数
set_as_attrbinary计算结果要写入的客户端属性名(受限字符串)

配置挂载在mqtt配置根之下,按 zone 生效,emqx_schema.erl中将其声明为数组(见 emqx_schema.erl)。一个典型的 HOCON 配置如下:

mqtt { client_attrs_init = [ { expression = "cert_common_name" set_as_attr = "tls_cn" }, { expression = "cert_subject" set_as_attr = "tls_subject" } ] }

执行链路:表达式在连接阶段如何被渲染

client_attrs_init的实际执行发生在 MQTT 连接处理流程中,核心逻辑位于 emqx_channel.erl。

1. 读取配置并按 zone 生效

get_client_attrs_init_config(Zone) -> get_mqtt_conf(Zone, client_attrs_init, []).

2. 组装渲染上下文并计算属性

maybe_set_client_initial_attrs(ConnPkt, #{zone := Zone} = ClientInfo, ConnInfo) -> case get_client_attrs_init_config(Zone) of [] -> {ok, ClientInfo}; Inits -> UserProperty = get_user_property_as_map(ConnPkt), Password = get_connect_password(ConnPkt), RenderCtx = client_attrs_init_render_ctx( ClientInfo#{user_property => UserProperty, password => Password}, ConnInfo ), Attrs0 = maps:get(client_attrs, ClientInfo, #{}), Attrs1 = initialize_client_attrs(Inits, RenderCtx), {ok, ClientInfo#{client_attrs => maps:merge(Attrs0, Attrs1)}} end.

从源码可以看出,渲染上下文(RenderCtx)除了clientinfo中的usernameclientiduser_propertypassword等基础变量外,还会叠加来自ConnInfo的证书信息。

3. 别名注入:cert_common_namecert_subject的来源

本次变更的核心实现就在这里,见 emqx_channel.erl:

client_attrs_init_render_ctx(Ctx, ConnInfo) -> client_attrs_init_render_ctx_cn(add_cert_san_render_ctx(Ctx, ConnInfo)). client_attrs_init_render_ctx_cn(#{cn := CN} = Ctx) -> client_attrs_init_render_ctx_dn(Ctx#{cert_common_name => CN}); client_attrs_init_render_ctx_cn(Ctx) -> client_attrs_init_render_ctx_dn(Ctx). client_attrs_init_render_ctx_dn(#{dn := DN} = Ctx) -> Ctx#{cert_subject => DN}; client_attrs_init_render_ctx_dn(Ctx) -> Ctx.

逻辑非常清晰:只要渲染上下文中存在cn,就同步注入cert_common_name => CN;只要存在dn,就同步注入cert_subject => DN。因此新旧两组变量完全等价,表达式里用哪一个都能取到相同的值。

4. 底层数据来源:证书的 CN 与 DN 从哪来

cndn本身来自对端(peer)TLS 证书的解析,见 emqx_channel.erl:

set_peercert_infos(PeerCert, ClientInfo, Zone) -> DN = esockd_peercert:subject(PeerCert), CN = esockd_peercert:common_name(PeerCert), ok = validate_peercert_string(dn, DN, PeerCert), ok = validate_peercert_string(cn, CN, PeerCert), ... ClientInfo#{username => Username, clientid => ClientId, dn => DN, cn => CN}.
  • cn(即别名cert_common_name):取对端证书的 Common Name(CN);
  • dn(即别名cert_subject):取对端证书的完整 Subject Distinguished Name(DN),如CN=client, O=EMQX

代码还通过validate_peercert_string/3对这两个字段做字节级校验,防止异常字符进入后续渲染流程(校验失败会触发{shutdown, peercert_field_invalid}拒绝连接),说明证书字段是可以被安全用于模板渲染和后续鉴权逻辑的。

另外,渲染上下文还会额外注入cert_san(证书 Subject Alternative Name,按dns/ip/email/uri分组),见 emqx_channel.erl,可用于按 SAN 信息初始化属性。

变量清单:client_attrs_init表达式中可用的证书相关变量

结合占位符头文件 emqx_placeholder.hrl 与上述执行逻辑,证书相关变量整理如下:

变量含义说明
cn证书 Common Name原有变量
cert_common_name证书 Common Name本次新增的cn别名
dn证书完整 Subject DN原有变量
cert_subject证书完整 Subject DN本次新增的dn别名
cert_san.dns/cert_san.ip/cert_san.email/cert_san.uri证书 SAN 各类型值列表形式

头文件同时声明了宏与二进制占位符定义,例如?PH_CERT_CN_NAME对应${cert_common_name}?PH_CERT_SUBJECT对应${cert_subject}(见 emqx_placeholder.hrl),说明这两个名字不仅是client_attrs_init的局部约定,而是整个占位符体系中的一等公民,其他支持占位符的模块同样可以引用。

实战:把证书 CN 提炼为客户端属性并用于 ACL

场景一:将证书信息写入客户端属性

假设需要把 TLS 客户端的证书 CN 与完整 Subject 提取为属性tls_cntls_subject,配置如下:

mqtt { client_attrs_init = [ { expression = "cert_common_name" set_as_attr = "tls_cn" }, { expression = "cert_subject" set_as_attr = "tls_subject" } ] }

连接建立后,这两个属性会出现在客户端的client_attrs中,可通过 Dashboard 或emqx_cm通道信息查看。

场景二:在 ACL 规则中按证书 CN 授权

客户端属性初始化后即可被鉴权模块引用。在基于文件的 ACL 配置(acl.conf)中,占位符${client_attrs.NAME}会被渲染为对应属性值,例如:

{allow, all, publish, ["${client_attrs.tls_cn}/#"]}

acl.conf的注释还单独列出了${cert_common_name}占位符,可直接在规则中引用证书 CN 而无需先写入属性:

{allow, all, publish, ["${cert_common_name}/#"]}

需要留意的是,注释中明确指出:当引用的值不存在时,占位符不会被渲染为空字符串,例如客户端没有group属性时,${client_attrs.group}/#不会退化为/#——这一安全行为同样适用于证书变量(未启用 TLS 或客户端未携带证书时,cert_common_name/cert_subject不可用)。

测试验证:新别名与旧变量行为一致

新增别名有对应的自动化测试用例覆盖,见 emqx_client_SUITE.erl:

  • t_certcn_as_alias/1t_certdn_as_alias/1:验证原有cndn
  • t_cert_common_name_as_alias/1t_cert_subject_as_alias/1:验证本次新增的cert_common_namecert_subject

四组用例共用同一套断言逻辑test_cert_extraction_as_alias(Which),其核心步骤是:

{ok, Compiled} = emqx_variform:compile("substr(" ++ atom_to_list(Which) ++ ",0,2)"), emqx_config:put_zone_conf(default, [mqtt, client_attrs_init], [ #{expression => Compiled, set_as_attr => <<"alias">>} ]), %% 通过 mTLS 连接 8883 端口 {ok, Client} = emqtt:start_link([ {clientid, ClientId}, {port, 8883}, {ssl, true}, {ssl_opts, SslConf} ]), {ok, _} = emqtt:connect(Client), ?assertMatch( #{clientinfo := #{client_attrs := #{<<"alias">> := <<_, _>>}}}, emqx_cm:get_chan_info(ClientId) )

用例以 mTLS 客户端连接 8883 端口,对cndncert_common_namecert_subject分别执行substr(x, 0, 2)表达式,断言最终写入的属性值为两个字符——四种变量取值行为完全一致,从测试层面印证了别名的等价性。

使用建议与注意事项

  1. 新旧变量可混用,建议新配置优先使用长名cert_common_namecert_subject语义自明,在团队协作与后续维护中更不易产生歧义;旧配置中的cn/dn无需迁移,完全兼容。
  2. 仅在 TLS/mTLS 连接下有意义:两个证书变量依赖对端证书,非 TLS 监听器或未携带证书的客户端不会提供这些值,表达式引用时需考虑缺失场景。
  3. 属性名受restricted_string校验约束set_as_attr指定的属性名必须满足受限字符串规则,配置非法时会在启动阶段即被拒绝。
  4. 表达式的表达能力expression采用 variform 语法,支持substr等字符串函数,可对 CN/DN 做截取、拼接等预处理后再落为属性,满足更细粒度的派生需求。

本次变更虽小,却补齐了client_attrs_init证书变量的语义化命名,配合属性初始化与 ACL 占位符渲染,为基于 TLS 客户端证书的细粒度接入控制提供了更清晰的配置表达。

  • 后端
  • 物联网
  • 消息队列
  • 通信

【免费下载链接】emqx

The most scalable and reliable MQTT broker for AI, IoT, IIoT and connected vehicles

项目地址:https://gitcode.com/gh_mirrors/em/emqx
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询