news 2026/9/30 10:39:14

EMQX ACL实战:多租户MQTT权限控制、外部授权与排障

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
EMQX ACL实战:多租户MQTT权限控制、外部授权与排障

1. 从一次"消息串台"事故说起:ACL到底在EMQX里管什么

场景先摆出来。我们用EMQX做了一个多租户的物联网平台,十几个客户的设备共用一套Broker,靠主题前缀(tenantA/、tenantB/这样的)区分。上线前只做了客户端认证,设备能不能连上来是验证过的,功能测试也全过。结果有一天客户A的运维同学在自己的客户端里顺手订阅了一个#,把全平台所有租户的数据都拉到了自己的机器上。数据没泄露到外部,但这个事本身就足够让所有人后背发凉。

那次之后我才真正把EMQX的ACL(Access Control List,访问控制列表)当成一件必须做的事,而不是"有空再说"的配置。这一篇就把我在ACL上踩过的、想明白的东西完整讲一遍。

先说清楚ACL和认证的区别,这是最多人混的一块。认证(Authentication)回答的是"你是谁",你看EMQX里的认证配置,password-based、JWT、客户端证书,全都是在校验身份。ACL回答的是"你能做什么",在身份已经确认之后,判断这个客户端能不能发布某个主题、能不能订阅某个主题。这两个是两道独立的关卡:认证不过,连接直接被拒;认证过了但ACL不通过,连接是建立的,但发布会被静默丢弃、订阅会被拒绝或被裁掉。很多新手觉得"我都配了用户名密码了,应该够安全了吧",实际情况是,一个合法账号可以把你整个Broker的消息全订阅走,只要主题写得够宽。

ACL的核心对象是客户端和主题。EMQX里每一条ACL规则基本就是这句话的结构:"某个客户端,允许/拒绝,对某类主题,做发布/订阅/两者都做"。注意客户端这一侧在ACL里可以用很多维度来匹配,用户名、clientid、IP地址、甚至客户端属性(比如证书里的CN),这才是ACL灵活的地方。主题那一侧则用字符串匹配,支持通配符。

EMQX的ACL有三层可以下手,这也是它相比纯自建权限校验更省事的地方。第一层是全局默认权限,一个默认允许还是默认拒绝的开关,管的是"所有规则都不命中时怎么办"。第二层是规则列表,可以在Dashboard上可视化地加规则,也可以走配置文件。第三层是外部数据源授权,把规则存到MySQL、PostgreSQL、Redis、MongoDB这些地方,客户端量大了以后必然要走这条路。这三层是叠加的,理解它们的优先级关系比记住某一条规则的写法更重要。

还有一个必须提前分清的概念:发布权限和订阅权限是两套独立的判断逻辑,而且作用机制不一样。发布的时候,客户端往某个主题发消息,EMQX拿这个主题去匹配规则,命中deny就丢掉,注意是安静地丢掉,客户端那边通常是没有任何报错的,消息就是不见了。订阅的时候,客户端发subscribe请求,EMQX会拿请求的主题去匹配;如果整个订阅都是deny,会返回一个失败码;但还有一种情况是"主题过滤器部分允许",这时候EMQX会把订阅裁剪成一个子集,客户端以为自己订阅了sensor/#,实际上后端只给它订阅了sensor/room1/#。这个裁剪行为是排查问题时最容易把人绕晕的地方,后面单独展开。

从设计视角看,ACL在EMQX里的定位是"数据平面的最后一道闸"。MQTT本身是个极简协议,它没有在协议层规定权限模型,只定义了CONNECT时能不能带用户名密码、SUBSCRIBE时Broker可以返回失败码。具体怎么控制,完全交给Broker实现。EMQX选择把这个能力做成可配置的ACL,而不是硬编码,好处是灵活,代价就是配置的复杂度全转移到使用者身上了。你不配,它就按默认来,而默认的那个值往往是"允许一切",这才是最危险的。

我个人的经验是,ACL这件事的上线顺序应该是:先把默认权限改成deny,再一条一条加allow规则,边加边测。反过来做——先默认allow,想着"我后面再收紧"——几乎一定会留下一堆没人记得的宽规则。这个顺序看起来反直觉,因为改完deny你会发现平台当场就瘫了,但这恰恰说明你之前的"安全"是靠默认放行撑着的假象。先让它瘫,再从最小可用权限一条条加回来,这个过程本身就是把权限模型梳理清楚的过程。

2. 三条判定链路:EMQX如何决定一次发布或订阅是否放行

要配得准,得先知道EMQX内部是怎么做判断的。这套判定链路我第一次读文档时是囫囵过去了的,后来排一个"明明配了allow还是不通过"的问题,才回头把它彻底捋清楚。捋清楚之后再配ACL,基本就是照方抓药。

2.1 判定顺序与优先级:谁先说话

EMQX处理一条ACL请求时,是按顺序依次查找的,找到第一个决定的规则就停。顺序大致是:外部授权数据源 → 内置规则列表(Dashboard或配置文件的规则) → 默认权限。这里要注意"停止"这个行为,它是短路式的:如果外部数据源返回了一个deny,后面的内置规则根本不会被看到,哪怕你的内置规则里有一条更宽松的allow。这个设计合理,因为越靠前的越"专用",但配错了就会产生"我改的规则怎么不生效"的困惑。

内置规则列表内部的顺序也有讲究,而这一点不同大版本之间有差异,我踩过。早期版本的规则列表里,deny优先于allow,也就是说同一批规则里只要有一条deny命中,不管allow写在它前面还是后面,都按deny。后来的版本改成了按列表顺序逐条匹配,命中即返回,谁在前面谁先生效。这个改动本身是好事,因为它给了你控制权,你可以在一条宽泛的deny前面加一条精确的allow来做例外。但如果你从旧版本升级上来、规则列表又写得很随意,行为就变了。

所以我的建议很直接:不要依赖"deny优先"这个隐式约定,永远假设是顺序匹配,然后自己把顺序排对。把具体的、精确的规则放前面,把宽泛的、兜底的放后面。比如:

allow user=alice topic=sensor/alice/# action=pubsub deny user=alice topic=sensor/# action=pubsub allow user=alice topic=cmd/alice/# action=sub

这段的意思是:alice能自由操作自己的那一小片主题,对于其他传感器主题一律拒绝,但还额外允许她订阅自己的命令主题。如果顺序反了,第二条deny会先命中,后面两条allow就都白写了。这种顺序敏感是必修课,别指望工具帮你自动排序。

2.2 主题匹配的细节:通配符与变量占位

主题匹配这一块,坑比想象的多。EMQX的ACL主题字段里能用的东西有两类,一类是MQTT原生通配符+和#,另一类是EMQX自己扩展的主题占位符,比如%u(用户名)、%c(clientid)。

先说MQTT原生通配符。+匹配单层,#匹配多层且必须在末尾。ACL规则里的主题写法要跟真实主题的层级结构对齐。很多人写sensor/#以为能匹配sensor这个主题本身,实测是不能的,#至少要多匹配一层,sensor不带子路径就匹配不上,得写成sensor和sensor/#两条,或者反过来干脆别用无子级的主题。这种边界细节,测试的时候一定要拿真实客户端发一遍验证,别只看文档。

然后是占位符,这是EMQX ACL真正好用的地方。topic=%u/up表示"每个客户端只能用自己用户名开头的那一层"。alice连上来,这条规则对她来说等价于alice/up;bob连上来就是bob/up。这一条规则顶替了所有按用户展开的规则,配起来极其省事。占位符主要有这几个:

占位符含义典型用法
%u用户名topic=%u/#造出一人一片的私有空间
%c客户端ID设备级隔离,topic=dev/%c/cmd
%a客户端IP按网段或固定IP授权

占位符的坑在于:如果客户端的用户名是空的(匿名连接),那么%u展开出来就是空的,topic=%u/#会变成topic=/#,这个匹配范围会变得很奇怪。所以用占位符的前提是认证层必须强制要求用户名存在,匿名连接要关掉。匿名连接一旦开着,占位符就成了一个洞。

2.3 订阅时的主题裁剪:最迷惑的行为

这是我认为EMQX ACL里最需要专门记住的一点。当客户端订阅一个带通配符的主题过滤器,而ACL只允许其中一部分时,EMQX不会直接拒绝,而是把订阅裁剪成允许的那部分。

举个例子。alice订阅#,但ACL只允许她订阅sensor/alice/#。那么EMQX实际给她订阅的就是sensor/alice/#,SUBACK返回的是成功。alice的客户端以为自己订了#,实际上什么都收不到除了自己那部分。这个行为在多客户端场景下非常容易误导排查:你看到客户端订阅成功、你看到Broker没有报错、你看到别人发的消息就是收不到,你会怀疑网络、怀疑QoS、怀疑retain,唯独想不到是订阅被悄悄裁剪了。

排查这种问题,最有效的办法是去EMQX的Dashboard里看当前订阅列表。点进客户端详情,能看到它实际生效的订阅过滤器是什么,是不是你请求的那个。如果列表里显示的是裁剪后的主题,那答案就找到了。我建议凡是ACL配完后出现"订阅成功但收不到消息",第一步就看这个列表,比翻日志快得多。

对应的还有发布侧的一个类似现象:发布被deny,客户端通常收不到任何错误,MQTT协议里PUBLISH就没有返回错误码的机制(QoS 0更是完全没法反馈)。所以"消息发出去了但对端没收到"这个问题,在ACL场景下十有八九是发布被拒绝了,而且这事没有任何客户端侧的动静。要确认,就得开EMQX的授权拒绝日志或者debug级别日志去看。

2.4 默认权限的两副面孔:deny与allow

默认权限这个开关,位置不起眼,威力巨大。EMQX里它有两个可选值,deny和allow。

默认allow的意思是"所有规则都不命中时放行"。这是很多示例配置的默认状态,也是导致串台问题的根源——你只写了几条deny,剩下的海量主题全在"不命中即放行"的管辖下。默认deny则相反,不命中就拒。

生产环境我强烈建议默认deny,然后所有需要放行的都用显式规则写出来。这样你的配置天然是白名单模型,新增一个主题类型必须显式加规则,虽然麻烦,但每一个权限都是有意为之的。这个麻烦是值得的,出事故的时候你就知道为什么了。

有个实际的注意点:改成默认deny之后,共享订阅、系统主题($SYS/#)、某些内部通信可能需要额外的放行规则,否则你会在健康监控上看到奇怪的空数据。切换默认权限后,先把这类"基础设施主题"的规则补上,再逐业务补,避免上来就一锅黑。

3. 从零把一套多租户ACL配出来

理论讲够了,进实操。这一节假设你已经在EMQX里配好了用户名密码认证,现在要做的是在认证的基础上,加一套多租户的发布订阅权限。我会按"先小后大、先测后放"的顺序来,中间会把每步的理由说清楚。

3.1 动手前的三个决定:默认权限、命名约定、占位符策略

坐下来开始写规则之前,先做三个决定,这三个决定之后就不用改了,改起来代价大。

决定一:默认权限设deny。理由前面说了,白名单模型才是可控的。这一步在Dashboard的授权页面,或者配置文件里对应authorization.no_match = deny。

决定二:主题命名约定固定下来。我的习惯是<租户>/<设备类型>/<设备ID>/<方向>,方向用up(上行,设备到平台)和down(下行,平台到设备)。命名约定不统一,ACL就没法用通配符批量授权,每个设备一套规则,维护成本爆炸。这个括号结构是我用下来最顺的,层级清晰、通配符好用、占位符好嵌。

决定三:用不用占位符。如果租户数少、用户固定,老老实实按用户名写规则反而更直观,出问题一眼能看明白第几条规则在管谁。如果租户多、设备动态接入、用户名本身就是设备ID,那必须用占位符,否则规则条数会失控。我的折中是:租户层级用%u占位符,设备层级暂时用具体规则,等设备量上来了再整体切到占位符。这样一开始规则可读,后面可扩展。

3.2 第一版规则:最小可用,只放行必须的

先看第一版,我给它写得很窄,只保证设备能干活:

allow username=alice topic=alice/+/+/up action=pub allow username=alice topic=alice/+/+/down action=sub

读懂它:alice这个账号只能往alice/设备类型/设备ID/up这种结构上发布,只能从.../down订阅。注意方向和主题段的对应是有意设计的,上行和下行走不同层,这样一条allow管发布、一条管订阅,权限颗粒度天然就分开了。比"pubsub一个主题"要清晰得多。

这两条规则放在Dashboard规则列表里,顺序无所谓,因为它们的主题不重叠。但一旦你开始加兜底deny,顺序就重要了。我第一版故意不加兜底deny,是因为默认权限已经是deny,兜底由它来做,规则列表只放需要放行的,读起来清爽。

3.3 用通配符处理设备ID不确定的情况

设备ID在接入前可能不知道,或者设备会新增。这时候alice/+/+/up里的第二个+就有用了——只要设备挂载在正确的租户和类型下,ID是什么都行。这就是用层级换灵活性的典型。

但要注意+的层数必须严格对齐。alice/+/up和alice/+/+/up是两回事,差一层就匹配不上。项目里经常出现"设备ID那一层有时候是空的"这种脏数据,导致alice/+/+/up匹配不上实际主题alice/sensor/up(少了一层)。这种情况要么在设备端保证主题层级高度固定,要么在ACL里把两种层级都写上:

allow username=alice topic=alice/+/+/up action=pub allow username=alice topic=alice/+/up action=pub

两条都写是为了兼容历史设备。这个"兜冗余"的写法看起来不优雅,但在设备固件不统一的现实里,比硬推固件升级靠谱。

另一个经常被忽略的点:+会匹配到 $SYS 那类系统主题吗?不会。MQTT规范里$开头的主题不参与#和+的通配匹配,这是规范层面的保护。所以你的用户规则写#也够不到系统主题,系统主题要单独的别名或规则来放行。这个是好事,省得误配把系统内部话题暴露了。

3.4 规则列表的顺序重排:把精确的放前面

设备多起来、规则多起来之后,规则列表的顺序必须手动管理。我现在的习惯是按"精确度"从上到下排列:

  1. 最上面:针对特定用户名、特定主题、特定动作的精确规则(例外规则放这,用来盖住下面的宽规则)
  2. 中间:带通配符的按租户规则(alice/+/+/up)
  3. 下面:跨租户的共享主题、公共只读主题
  4. 最后:如果一定要有兜底deny,放最下面

这个顺序的原因很简单:命中即返回,所以越靠上的优先级越高。例外规则(比如"某台特殊的设备可以跨租户读一条公共数据")必须放最上面,否则会被租户级的宽规则挡住。

举个我实际用到的例外:

allow username=monitor topic=public/status action=sub allow username=alice topic=alice/+/+/up action=pub allow username=alice topic=alice/+/+/down action=sub allow username=bob topic=bob/+/+/up action=pub allow username=bob topic=bob/+/+/down action=sub

第一行就是例外——监控账号要读全局状态,放在最前面。后面是各租户的规则。这样加规则的时候,脑子里的模型是"从上往下越来越宽",不容易乱。

4. 换成外部数据源:规则量大之后的必然选择

规则列表到几十条还能忍,到几百上千条就完全不行了,Dashboard页面卡、改一条规则要翻半天、还不敢批量改。这时候就得把规则搬到外部数据源里,EMQX支持MySQL、PostgreSQL、Redis、MongoDB等多种后端。这一节讲两种我实际用过的:MySQL和Redis,以及各自的坑。

4.1 用MySQL做授权:动态规则的最佳载体

MySQL适合规则"需要被后台管理系统增删改"的场景。EMQX里配一个MySQL授权器,你需要做两件事:建一张表,符合EMQX期望的字段结构;在EMQX里配一个SQL查询,把客户端的信息作为参数传进去。

表结构大概是这样的:

CREATE TABLE mqtt_acl ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(100), clientid VARCHAR(100), ipaddr VARCHAR(60), topic VARCHAR(200), action ENUM('pub','sub','pubsub') DEFAULT 'pubsub', permission ENUM('allow','deny') DEFAULT 'allow', INDEX idx_username (username), INDEX idx_clientid (clientid) );

字段名不一定要跟EMQX的示例完全一致,因为查询SQL是你自己写的,关键是你写的SELECT语句返回的列名要跟EMQX约定的一致,否则EMQX读不到。这里的坑我第一次踩得很实:我在SQL里写SELECT permission, topic, action FROM mqtt_acl WHERE ...,结果ACL死活不生效。排查了半天,问题在列的顺序或列名上,EMQX是按列名取值,大小写和拼写必须对。稳妥的做法是照着EMQX官方文档的示例SQL抄列名,别自己发挥。

查询SQL一般是这样的思路:把username、clientid、ipaddr三个维度都用上,哪个匹配就用哪条:

SELECT permission, action, topic, 'deny' AS permission FROM mqtt_acl WHERE (username = ${username} OR username IS NULL) AND (clientid = ${clientid} OR clientid IS NULL) AND (ipaddr = ${ipaddr} OR ipaddr IS NULL)

这段SQL的逻辑我解释一下:${username}这些是EMQX传进来的占位符,会被替换成当前客户端的信息。OR xxx IS NULL的意思是"这一列在数据表里是空的,就代表这条规则对所有人适用"。这样你可以写通配规则(username为空),也可以写精确规则(username填具体值),一张表搞定。

这里有个必须自己确认的行为:MySQL返回多条记录时,EMQX怎么处理?是任意一条allow就放行,还是必须全部allow?这跟版本有关,也跟你查询里加没加LIMIT有关。稳妥的做法是让查询保证只返回一条最匹配的记录,用ORDER BY排序精确度、加LIMIT 1,避免多条的歧义把判断搞成玄学。我给查询加了:

ORDER BY (username IS NOT NULL) DESC, (clientid IS NOT NULL) DESC LIMIT 1

按"越精确越优先"排,取第一条。这样规则表里可以同时存在通配规则和精确规则,精确的自动盖过通配的。这个设计比让EMQX去猜多条记录怎么合并要靠谱得多。

缓存必须开。EMQX对授权查询有缓存机制,不开的话每个PUBLISH、每个SUBSCRIBE都去查MySQL,量一大数据库立刻顶不住。缓存的意义是:第一次查询完记在内存里,后面同样的key直接读内存。但这也带来一个副作用——你在数据库里改了规则,客户端不会立刻生效,要等缓存过期,或者在Dashboard里点"清除授权缓存"。这个我踩过一次,改完规则测了半天发现不生效,差点以为规则写错了,实际是缓存没清。养成习惯:改规则后先清缓存再测。

4.2 用Redis做授权:更快,但更考验设计

Redis相比MySQL胜在快,适合规则变化频繁、对延迟敏感的场景。EMQX的Redis授权器支持几种数据组织方式,最常用的是把规则存成一个Hash或者字符串,用客户端信息拼key去查。

我的用法是按用户名拼key,散列结构存action/topic/permission:

HSET acl:alice up "pub alice/alice/+/+/up allow" HSET acl:alice down "sub alice/alice/+/+/down allow"

然后在EMQX里配查询,GET acl:${username}或者HGETALL acl:${username},把取到的字符串按EMQX期望的格式拼好返回。Redis授权器对返回格式极其敏感,那个字符串的语法是EMQX自己定的,字段间怎么分隔、action和topic的顺序,错一个空格就整条规则不生效,而且多半不报错,只是静默不匹配。这是我用Redis授权最难受的地方——没有反馈。

所以用Redis的话,我的建议是先在Dashboard里用内置规则做出一版能跑通的规则,再照着这条能跑的规则,一比一去翻译成Redis里的字符串。翻译完拿一个测试客户端发一条消息验证,通过了再批量铺开。别一上来就写十条Redis规则指望靠一次测试全通。

还有一点要注意:Redis的key设计决定维护效率。按username拼key,删某个租户的全部权限就是删一个key,很痛快;但想"列出所有租户"就得用SCAN遍历,Dashboard里看不到。如果想要可视化地管理,还是MySQL更舒服。Redis适合的是规则简单、追求性能、由程序自动写入的场景(比如权限从业务系统同步过来)。

4.3 两者对比与选型建议

把两种方式放一起看,选型其实很清晰:

维度MySQL授权Redis授权
规则变更生效需清缓存,稍有延迟依赖key过期,或手动清
查询延迟毫秒级,量大有压力亚毫秒级,抗压好
可维护性好,能SQL查、能后台改一般,格式敏感、无可视化
适合场景后台可管的动态规则程序自动写入的高频规则
出错反馈查询报错相对可见基本静默,排查难
配置复杂度中,要懂SQL和表结构中高,要摸清字符串格式

我的结论是:中小规模、规则要人管的,选MySQL;大规模、规则由系统生成的,选Redis。别为了"性能更好"硬上Redis,结果可维护性差到没人敢改规则,那是得不偿失。

5. 排障实录:从现象倒推ACL规则的问题

ACL配好了不等于没坑,坑在"出问题的时候你不知道是哪条规则在管"。这一节把三个我真实遇到过的现象、排查链路、和修复方案完整走一遍,目的是让你遇到类似的能照着推。

5.1 现象一:订阅成功,消息收不到

现场:新接了一个租户,用户名和权限都配了,客户端能连、能订阅自己的主题,但就是收不到平台下发的命令。

排查链路:

  1. 先看客户端订阅返回,是SUBACK成功,排除订阅被拒。
  2. 平台侧确认命令确实发出去了,用另一个能收到该主题的客户端验证,能收到,说明消息本身没问题。
  3. 进Dashboard,点该客户端的详情,看实际订阅列表。发现列表里显示的过滤器是tenantC/+/cmd这样一个裁剪后的形式,而不是客户端请求的那个更宽的主题。
  4. 结论:订阅被裁剪了。客户端的请求主题比ACL允许的范围宽,EMQX给裁成了允许的那部分,而平台下发的主题刚好落在被裁掉的区域外。

根因:客户端请求的主题过滤器和ACL允许的主题层级没对齐。客户端订tenantC/#,但命令主题是多一层还是少一层,导致裁剪后的子集不包含它。

修复:把ACL里的主题和客户端订阅的主题对齐。具体到我们的情况,是平台下发命令的主题层级比设备订阅的多了个中间层,改ACL为tenantC/+/+/cmd、或者改设备订阅为对齐的层级,二选一。我们选了改ACL,因为改设备意味着固件升级,成本高。

教训:主题层级设计定下来之后不要轻易加层。加一层看起来是小事,会把所有ACL规则和所有设备的订阅全部打乱。要加层,先评估对权限规则的影响。

5.2 现象二:发布静默失败

现场:设备上报数据,平台侧迟迟收不到,设备日志显示PUBLISH已经发出(QoS 0,没有确认机制,设备以为发成功了)。

排查链路:

  1. 设备端换QoS 1重试,看PUBACK。如果收到PUBACK,说明发布成功;收不到才可能有问题。但注意QoS 1收到PUBACK也不代表ACL放行——ACL拒绝是Broker内部的事,回不回PUBACK要看实现。所以这条不能定论。
  2. 换工具,用MQTTX以同样的用户名手动发一条同样的主题,观察是否成功。
  3. 打开EMQX的授权拒绝日志(debug级),复现操作,看日志里有没有对应的拒绝记录。

根因:设备发布主题的设备ID层和ACL规则里的不匹配。设备上报时用的是从注册表读的设备ID,但ACL里我按"租户/类型/设备ID"三层写的,而设备实际发的是"租户/设备ID"两层(不带类型层),导致+/+/up匹配不上两层主题。

修复:加一条兼容规则tenantD/+/up action=pub。或者统一设备端主题生成逻辑,但这也涉及固件,所以先加规则兜着。

教训:发布侧的错误是无声的,这是MQTT协议带来的,不是EMQX的缺陷。所以ACL上线后,"消息丢了"要优先怀疑ACL,而不是网络。开拒绝日志是最快的定位手段。

5.3 现象三:规则改了不生效

现场:用MySQL授权,发现某条规则写错了,在数据库里改好了,等了几分钟,行为还是老样子。

排查链路:

  1. 先怀疑SQL:把EMQX配置的SQL拿到MySQL客户端里手动执行,${username}换成实际值,看返回是否符合预期。返回对,排除SQL问题。
  2. 怀疑缓存:EMQX的授权缓存还在用旧结果。去Dashboard清授权缓存,或者重启授权器的缓存。
  3. 再看行为是否变了。变了,问题确认是缓存。

根因:授权缓存的TTL还没到,EMQX还在用旧的查询结果。

修复:清缓存。并且把这件事变成流程——规则变更后,第一步就是清授权缓存,再验证。如果缓存TTL设得很长,规则变更的影响窗口就很长,要根据业务容忍度调TTL。

教训:任何带缓存的权限系统,"改完不生效"先查缓存。这条经验适用于所有中间件,不只是EMQX。

5.4 排障工具清单

把上面用到的手段整理一下,这几样备着,ACL的问题基本不用慌:

  • Dashboard客户端详情:看实际订阅列表,识别订阅裁剪。
  • 授权拒绝日志(debug级):看发布/订阅被谁拒了。
  • MQTTX:用相同身份手动复现,排除设备端问题。
  • 数据库客户端:把EMQX的SQL手动跑一遍,验证查询本身。
  • 缓存清理入口:改规则后必做的第一步。
  • 订阅与发布的原始报文抓取:客户端侧抓包看收发的主题到底是什么,避免"我以为它发的主题"这种一厢情愿。

我现在的习惯是,每给一个租户配好ACL,先用MQTTX用该租户的身份走一遍"发布—订阅—收到"的完整链路,再交给设备测试。这个人工验证只花几分钟,但能挡掉绝大多数"配了但配错"的情况。

6. 几个只有踩过才懂的细节与检查清单

写到这里,规则写法、数据源、排障都覆盖了。最后收几个零碎但很关键的细节,这些是文档里不会重点提、但实操中一定会遇到的东西。

第一,认证和ACL的衔接必须闭合。匿名连接一定要关。只要匿名能连,你的ACL再严密也挡不住"用空用户名连进来"的客户端,因为占位符%u会展开成空,规则可能有意外的匹配。这个检查我在每个环境上线前都会做一遍,很简单,但效果立竿见影。

第二,共享订阅要单独授权。共享订阅的主题带$share前缀,它跟普通订阅是两码事。用共享订阅做负载均衡的架构里,ACL得单独给$share/...放行,否则订阅静默失败,表现为"多实例部署了但只有一个实例在消费",排查起来很费时间。

第三,系统主题不要随便开。$SYS/#里有大量Broker运行信息,客户端一般不需要看。除非有专门监控的账号,否则系统主题的订阅权限不要放开,放开了就是个信息泄露面。

第四,$前缀的兼容要注意。前面提过MQTT规范里$主题不参与通配符匹配,但不同Broker、不同版本实现上可能有细微差异。涉及系统主题、共享订阅主题的规则,不要用#去"顺带覆盖",老老实实写完整、明确的主题前缀。

第五,配完必做的验证链。我在每个环境配完ACL后固定走这几步,你可以直接抄:

  1. 用预期应该放行的身份,走一遍发布和订阅,确认都能通;
  2. 用预期应该被拒的身份(比如另一个租户),尝试订阅别人的主题,确认被拒;
  3. 用宽泛主题(如#)订阅,确认实际被裁剪成预期范围,且收不到别人的消息;
  4. 看拒绝日志,确认放行的没有意外产生拒绝记录;
  5. 如果用了外部数据源,改一条规则、清缓存、再跑第1到3步。

这五步走完,基本能确认ACL是按设计工作的。我见过太多"配了规则但没验证"的情况,上线后靠事故来发现问题,成本和验证这几分钟完全不对等。

第六,规则文档化。规则多了以后,光看主题字符串很难知道"这条规则是给哪个业务、为什么这么配的"。我的做法是在MySQL授权方案的注释列或者一个单独的文档里,给每条规则记一句"为什么"。设备换型号、业务调整的时候,这条注释能救你——让你知道哪条规则能动、哪条动了会影响什么。

我个人在这一块的心得是,ACL真正的难度不在语法,在于心里要有一张完整的权限地图:哪个身份、能碰哪些主题、能做哪些动作、边界在哪。工具(Dashboard、SQL、Redis)只是执行这张地图的手段。地图没想清楚,配出来的规则一定是东一块西一块,上线之后改一处崩一片。所以我现在的流程是,配ACL之前先在纸上把权限矩阵画出来,谁对什么有权限一目了然,然后才是往EMQX里填。这个前置工作花的时间,会在后面每一次改规则的时候加倍还回来。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 10:36:36

GEO内容工具怎么选?6个维度实测

生成式引擎优化&#xff08;GEO&#xff09;的落地&#xff0c;核心离不开内容工具的支撑。不少运营者在搭建账号矩阵时&#xff0c;会纠结工具选型&#xff1a;有的工具擅长批量撰稿&#xff0c;有的侧重关键词挖掘&#xff0c;还有的主打多平台一键分发。盲目采购不仅浪费预算…

作者头像 李华
网站建设 2026/9/30 10:36:30

卫星互联网安全:Starlink用户链路IP欺骗检测与防御实战

简介&#xff1a;这份PDF面向网络安全研究者、卫星通信从业者及CTF-Misc方向学习者&#xff0c;聚焦Starlink用户链路流量中的IP欺骗防御问题&#xff0c;系统梳理卫星互联网安全威胁与应对思路。资源为单份PDF文档&#xff0c;共4.56MB&#xff0c;支持目录章节跳转与阅读器左…

作者头像 李华
网站建设 2026/9/30 10:36:20

MQTT协议详解:报文结构、QoS语义与Broker选型避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 10:34:10

C++ auto类型推导原理与高阶工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 10:31:55

YOLOv11工业零件表面缺陷检测实战:小目标优化与TensorRT部署全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华