- 后端
- 物联网
- 消息队列
- 通信
【免费下载链接】emqx
The most scalable and reliable MQTT broker for AI, IoT, IIoT and connected vehicles
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 规则中实际使用这两个新别名。
变更概述:一次清晰的别名增强
本次变更的内容非常聚焦,原文只有一句话:
Added
cert_common_nameandcert_subjectaliases formqtt.client_attrs_initexpressions, alongside the existingcnanddnvariables.
即:在mqtt.client_attrs_init表达式中,新增cert_common_name、cert_subject两个变量,作为已有变量cn、dn的别名,二者可以互换使用。别名引入后,配置语义不再依赖两个含义模糊的缩写,而是直接表达“证书通用名”与“证书主题”,降低了配置的可读性与理解成本。
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是一个配置项列表,列表中的每一项包含两个字段:
| 字段 | 类型 | 说明 |
|---|---|---|
expression | variform 表达式 | 计算属性值的表达式,可引用连接阶段的各类变量并调用字符串处理函数 |
set_as_attr | binary | 计算结果要写入的客户端属性名(受限字符串) |
配置挂载在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中的username、clientid、user_property、password等基础变量外,还会叠加来自ConnInfo的证书信息。
3. 别名注入:cert_common_name与cert_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 从哪来
cn与dn本身来自对端(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_cn、tls_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/1、t_certdn_as_alias/1:验证原有cn、dn;t_cert_common_name_as_alias/1、t_cert_subject_as_alias/1:验证本次新增的cert_common_name、cert_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 端口,对cn、dn、cert_common_name、cert_subject分别执行substr(x, 0, 2)表达式,断言最终写入的属性值为两个字符——四种变量取值行为完全一致,从测试层面印证了别名的等价性。
使用建议与注意事项
- 新旧变量可混用,建议新配置优先使用长名:
cert_common_name与cert_subject语义自明,在团队协作与后续维护中更不易产生歧义;旧配置中的cn/dn无需迁移,完全兼容。 - 仅在 TLS/mTLS 连接下有意义:两个证书变量依赖对端证书,非 TLS 监听器或未携带证书的客户端不会提供这些值,表达式引用时需考虑缺失场景。
- 属性名受
restricted_string校验约束:set_as_attr指定的属性名必须满足受限字符串规则,配置非法时会在启动阶段即被拒绝。 - 表达式的表达能力:
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
相关推荐
EMQX client_attrs_init 支持 password 变量:用 JWT 密码初始化客户端属性
EMQX client_attrs_init 支持 password 变量:用 JWT 密码初始化客户端属性 导读 本篇文章围绕 EMQX 开源仓库中的变更记录
后端物联网消息队列通信EMQX 从 TLS 客户端证书提取 Subject Alternative Name(SAN)到 MQTT 客户端属性实战指南
EMQX 从 TLS 客户端证书提取 Subject Alternative Name(SAN)到 MQTT 客户端属性实战指南 本文介绍 EMQX 5.x/6
后端物联网消息队列通信Slang 初始化表达式与初始化列表表达式:语言规范、一致性测试与源码验证实战
Slang 初始化表达式与初始化列表表达式:语言规范、一致性测试与源码验证实战 导读 本文聚焦 Slang 着色语言中两个紧密关联的表达式特性—— 初始化表达式
编译器图形学编程语言
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考