拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

EMQX MQTT ACL 发布订阅权限配置与排障实战

EMQX MQTT ACL 发布订阅权限配置与排障实战

线上出过一次挺典型的故障:一台测试设备拿着完全合法的账号密码,把生产线的遥测主题整条订走了,监控大屏刷了一大屏不该它看的数据。账号体系没问题,密码也没泄露,问题卡在 MQTT 的 ACL 上——EMQX 装完之后默认是"认证过了就全放行",谁拿到账号,谁就能订阅全量主题。这件事之后我把整套 MQTT——EMQX 学习笔记重新梳理了一遍,今天单独把 ACL(发布与订阅权限)这块拿出来讲透。这篇内容偏实操,适合已经在用 EMQX 但还没真正管过权限的物联网开发、后端运维,以及正在做设备接入平台的同学;刚接触 MQTT 的新手也能看懂,因为我会把每条规则拆到字段级别。

1. 认证管"你是谁",ACL 管"你能干什么"

1.1 一次典型事故引出的权限盲区

很多人搭 MQTT 服务器的路径是:装 EMQX → 建个用户名密码 → 客户端连上 → 能收发消息 → 上线。整条链路里压根没出现过"授权"这两个字。EMQX 的默认行为是:只要客户端通过了认证(或者压根没开认证),它对所有主题的发布和订阅请求都会被允许。这在实验室环境里很方便,一旦进入多租户或者多设备的真实场景,就是个裸奔状态。

我遇到的那次事故,根本原因就是所有设备共用一个账号,而 ACL 没配。设备 A 本该只发plant/line1/A001/telemetry,结果它订了plant/#,把三条产线的数据全收走了。

这里要先把两个概念分清楚。认证(Authentication)解决"你是谁",靠的是用户名密码、客户端证书、JWT 这一类凭据;授权(Authorization,也就是 ACL)解决"你能干什么",靠的是规则表,判断某个客户端能不能对某个主题执行发布或订阅。这两件事在 EMQX 里是两套独立配置,各管一摊,缺一不可。只做认证不做授权,等于给每个员工发了工牌,但办公室所有门都是敞开的。

1.2 ACL 的三要素:主体、动作、主题

不管你在哪个 MQTT 服务器上配 ACL,规则的本质都是同一个三段式判断:谁(主体)对哪个主题(资源)执行什么动作(发布/订阅),结果是允许还是拒绝。

主体可以是用户名,也可以是客户端 ID,还可以是来源 IP,或者干脆写"所有人"。动作只有三种:publish、subscribe、all。主题则是用 MQTT 主题过滤器表达的一段字符串,支持+和#通配符,也支持基于用户名、客户端 ID 的占位符,还能做精确匹配和正则匹配。

这三要素的排列组合,基本能覆盖设备接入侧的绝大多数需求。举几个真实场景:传感器只允许发布、不允许订阅,那就写"该设备 → 自己的主题 → publish 允许,其余拒绝";网关需要订阅一堆设备的数据、再往上游转发,那就给它subscribe的gateway/+/data加上publish的cloud/upstream;运维工具要读$SYS/#看服务器状态,那就单独给它开$SYS的订阅权限,其他设备一律禁止碰$SYS。想清楚三要素,规则基本就写出来了。

1.3 EMQX 里授权规则能挂在哪儿

EMQX 5.x 把授权做成了插件化的"数据源"结构,你可以挂一个,也可以按优先级挂一串。常见的有这么几类:

  • 内置数据库:规则存在 EMQX 自己的存储里,通过 Dashboard 的"访问控制 → 授权"页面增删改,改完立即生效,不用重启。
  • 文件(acl.conf):规则写在一个 Erlang 项格式的配置文件里,改动需要重载或重启,适合策略固定、数量不多的场景。
  • 外部数据源:HTTP 接口、MySQL、PostgreSQL、MongoDB、Redis 等。规则量大的时候,把权限表放进现成的数据库,用 SQL 或 Redis 命令查,运维体系能统一。
  • 客户端信息:某些版本支持直接从连接信息里取字段做判断。

多数据源同时开启时,EMQX 会按配置顺序依次查询,任意一个数据源返回了明确的 allow 或 deny 就立即终止,后面的不再查;全部数据源都返回"不匹配"时,才落到no_match这条兜底策略上。这个顺序逻辑很关键,后面排障那一节会反复用到。

2. 把 ACL 规则拆到字段级:语法、通配符与匹配顺序

2.1 一条规则由四个位置参数组成

先看acl.conf文件版的写法,因为它最直白,把规则结构暴露得最清楚。EMQX 5.x 的默认文件长这样:

%% 允许用户名为 dashboard 的用户订阅 $SYS 开头的主题 {allow, {username, {re, "^dashboard$"}}, subscribe, ["$SYS/#"]}. %% 允许来自本机的客户端发布和订阅所有主题 {allow, {ipaddr, "127.0.0.1"}, all, ["$SYS/#", "#"]}. %% 拒绝所有人订阅 $SYS 主题,以及用 # 做的全量订阅 {deny, all, subscribe, ["$SYS/#", {eq, "#"}]}. %% 其余请求一律允许 {allow, all}.

一条规则是四个位置参数,顺序不能乱:

  1. 权限:allow或deny。
  2. 主体:all表示所有人;{username, "xxx"}按用户名匹配;{clientid, "xxx"}按客户端 ID 匹配;{ipaddr, "192.168.1.0/24"}按来源网段匹配;想用正则就再包一层,写成{username, {re, "^sensor-\\d+$"}}。
  3. 动作:publish、subscribe、all三选一。
  4. 主题列表:一个列表,里面每一项都是一个主题过滤器,命中任意一项就算命中这条规则。列表里还能塞特殊形式,{eq, "xxx"}表示精确匹配(不做通配符解释),{re, "xxx"}表示正则匹配。

有个版本差异必须提醒:EMQX 4.x 里主体关键字写的是{user, "xxx"},5.x 改成了{username, "xxx"}。网上很多老教程还在用user,你直接抄到 5.x 上会发现规则死活不生效。写之前先确认自己跑的是哪个大版本,别在这上面浪费一晚上。

2.2 主题里的通配符、占位符与精确匹配

主题这一栏是最容易写错的地方,坑集中在这里。

先说通配符。MQTT 的+匹配单层,#匹配多层且必须在末尾。ACL 规则里的主题过滤器遵循同样的规则,所以sensor/+/temp能覆盖sensor/A001/temp和sensor/B002/temp,但覆盖不了sensor/A001/room1/temp。这里有个经典的认知偏差:ACL 判断的不是"客户端订阅的那个字符串",而是"这个订阅会触及哪些主题"。客户端发一个sensor/#的订阅请求,服务器要判断的是这条订阅一旦建立,它能收到哪些主题的消息,然后拿这些主题去匹配规则。所以如果你只给某用户开了sensor/+/temp的订阅权限,他去订sensor/#,很可能被拒,因为#的覆盖面远超授权范围。

再说占位符,这是让规则"活起来"的关键。文件版里用%u代表用户名,%c代表客户端 ID。比如:

%% 每个用户只能订阅自己名下的一层主题 {allow, {username, {re, "^dev-"}}, subscribe, ["devices/%u/#"]}. %% 每个客户端只能发布自己 ID 对应的主题 {allow, {clientid, {re, "^dev-"}}, publish, ["devices/%c/up"]}.

Dashboard 内置数据库那一侧,占位符的写法不一样,用的是${username}和${clientid}:

devices/${username}/# devices/${clientid}/up

这个差异我踩过,从文档里抄%u到 Dashboard 输入框里,保存成功、看着也对,就是不生效,因为内置数据库不认这个语法。两套语法各管各的,记住这个对应关系能省不少事。

至于{eq, "xxx"},它的作用是关掉通配符解释。默认情况下规则里的#是通配符,但如果你想表达"就是字面意义上的#这个主题",就得写成{eq, "#"}。EMQX 默认配置里那条{deny, all, subscribe, ["$SYS/#", {eq, "#"}]}就是这个意思:拒绝所有对$SYS开头的主题的订阅,同时专门拒绝"用#来全量订阅"这个行为——因为全量订阅等于把整个 broker 的消息都捞走,属于典型的高危操作。

2.3 匹配顺序、缓存与 no_match 的兜底逻辑

规则在文件里是按顺序排的,但EMQX 的匹配不是"第一条命中最先返回",而是有明确的优先级:具体说,在同一个数据源内部,规则从上往下逐条匹配,deny和allow谁先命中谁生效?这里官方的实际行为是:所有匹配的规则中,只要有一条是 deny,就拒绝;只有全部匹配项都是 allow,才允许。换句话说 deny 的优先级高于 allow。

这个设计是出于安全考虑,但也带来一个后果:你写了一条{allow, all}在最上面,后面再加{deny, ...},并不会因为"allow 在前面"就先放行,deny 依然会生效。所以allow all这条一定要放在最后当兜底,别放开头。

第二个关键点是no_match,也就是所有规则都没命中时的兜底策略。配置项是:

authorization { no_match = deny # 可选 allow / deny,默认 allow deny_action = ignore # 可选 ignore / disconnect,默认 ignore }

no_match默认是allow,这是最容易出事的一个默认值。它的含义是:如果你的规则表里没有一条能匹配上当前请求,那就放行。这意味着一个拼错的主题、一个没想到的用户名格式,都可能悄悄绕开你的整套规则。生产环境我建议直接改成deny,走白名单思路——只有明确写了的才允许,其余全拒。改完之后一定要把规则表补全,否则会出现"设备突然发不出消息"的情况。

第三个是缓存。EMQX 支持把授权结果缓存起来,避免每条消息都去查一遍数据源,尤其是走 HTTP 或数据库的时候:

authorization { cache { enable = true max_size = 32 ttl = 1m } }

缓存能显著降低授权开销,但也意味着改完规则不会立刻全量生效,最坏要等一个ttl周期。排障的时候如果发现规则改了但行为没变,先看看是不是缓存在起作用,把 ttl 调小或者临时关掉再验证。

2.4 deny_action:拒绝之后是丢包还是断线

deny_action这个参数决定了"被拒绝之后发生什么",很多人没注意它,结果被现象绕晕。

  • ignore(默认):发布被拒,消息静默丢弃,客户端不会有明显感知;订阅被拒,服务器返回订阅失败。
  • disconnect:一旦被拒绝,服务器直接断开这个客户端的连接。

选哪个取决于你的业务语义。如果客户端是那种写得很糙、会无脑重试的设备,用ignore比较温和,避免它陷入"被拒 → 断线 → 重连 → 再被拒"的循环风暴;但如果你希望权限问题暴露得足够明显,让开发和运维第一时间发现配错了,那disconnect更直接。我一般是先在测试环境用disconnect把问题打出来,规则调稳之后生产环境切回ignore。

还有一点,MQTT 5.0 的客户端在被拒的时候能拿到明确的原因码,比如发布被拒会收到PUBACK里带的0x87 Not authorized,订阅被拒会在SUBACK里看到对应失败码。MQTT 3.1.1 则只有SUBACK的0x80这个笼统的失败码,发布被拒时 QoS 0 的消息更是连回执都没有,完全静默。这就是为什么同样一套规则,用 MQTT 5.0 的客户端排障会轻松很多——它至少会告诉你"我被拒了",而不是让你对着没有消息的界面发呆。

3. 三种主流落地方式:文件、内置数据库、外部数据源

3.1 acl.conf 静态文件:小而稳,适合设备侧固定策略

文件版的优点是不依赖任何外部组件,重启即加载,规则一目了然,能进版本管理。缺点是改动要重载配置,没有界面,规则一多就难维护。

启用方式是在配置文件里把 file 数据源打开:

authorization { sources = [ { type = file enable = true path = "etc/acl.conf" } ] }

一个相对完整的设备侧策略可以这么写:

%% 允许名为 collector 的服务端账号订阅所有设备上行主题 {allow, {username, "collector"}, subscribe, ["devices/+/up"]}. %% 允许名为 collector 的账号向下发指令 {allow, {username, "collector"}, publish, ["devices/+/down"]}. %% 每台设备只能发布自己的上行主题 {allow, {clientid, {re, "^dev-"}}, publish, ["devices/%c/up"]}. %% 每台设备只能订阅发给自己下行的主题 {allow, {clientid, {re, "^dev-"}}, subscribe, ["devices/%c/down"]}. %% 运维账号可以看服务器自身状态 {allow, {username, "ops"}, subscribe, ["$SYS/#"]}. %% 兜底:明确拒绝 {deny, all}.

注意最后那条{deny, all},这是把兜底从allow换成显式拒绝。写这条的前提是你的规则表已经覆盖了所有合法行为,否则会出现莫名其妙的"设备连上了但发不出数据"。我的建议是先在测试环境跑一轮全量回归,确认没有漏网的合法请求,再上这条。

注意:用{deny, all}做兜底时,必须同时确认authorization.no_match的取值,两者语义上有重叠。规则写全了就没问题,写不全就会两边一起拒。

3.2 Dashboard 内置数据库:改规则不用重启

内置数据库是我在中小规模场景里最推荐的方案,因为它有图形界面、改完立即生效、支持动态增删,运维和开发都能上手。

操作路径是:登录 EMQX Dashboard → 左侧"访问控制" → "授权" → 确认开启了"内置数据库"数据源 → 在下方"权限"标签页里添加规则。

内置数据库里每条规则要填的字段,和文件版一一对应:

字段说明示例
权限允许 / 拒绝允许
用户名匹配连接时的 username,可留空dev-A001
客户端 ID匹配连接的 clientid,可留空dev-A001
IP 地址匹配来源网段,可留空192.168.10.0/24
操作发布 / 订阅 / 发布订阅发布订阅
主题主题过滤器,支持${username}、${clientid}devices/${clientid}/#

这里有个细节:用户名、客户端 ID、IP 地址这三个条件留空表示"不限制",填了才作为判断条件。多个条件同时填,是"与"的关系,必须全部满足才算命中。所以最常见的写法是"用户名填上、其余留空",一条规则对应一个账号。

提示:内置数据库的规则默认存储在 EMQX 的数据目录里。做集群的时候,规则是通过集群同步机制自动下发的,不需要每个节点手工加。但如果你是从单机扩成集群,建议先确认好数据目录和持久化配置,避免节点间规则不一致。

另一个提效的点是 Dashboard 提供了规则导入导出(在授权页面右上角),可以把一套规则批量导入到不同环境。测试环境调好的规则,导出 JSON 再导进生产,比手敲一遍靠谱得多。

3.3 对接 MySQL / Redis:规则量大时的选择

当设备量上到几千上万,或者你的权限本身就和业务系统的用户表、设备表关联时,把 ACL 放进现成数据库更合理——业务系统改权限,MQTT 侧自动跟着变,不用两头维护。

MySQL 的思路是准备一张权限表,然后配置一条带占位符的查询语句。表结构可以这样设计:

CREATE TABLE mqtt_acl ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(100) DEFAULT NULL, clientid VARCHAR(100) DEFAULT NULL, ipaddr VARCHAR(60) DEFAULT NULL, permission VARCHAR(10) NOT NULL, -- allow / deny action VARCHAR(10) NOT NULL, -- publish / subscribe / all topic VARCHAR(200) NOT NULL, -- 主题过滤器 INDEX idx_username (username), INDEX idx_clientid (clientid) );

插入几条规则:

INSERT INTO mqtt_acl (username, permission, action, topic) VALUES ('collector', 'allow', 'subscribe', 'devices/+/up'), ('collector', 'allow', 'publish', 'devices/+/down'), ('ops', 'allow', 'subscribe', '$SYS/#');

EMQX 侧的查询语句里可以用${username}、${clientid}、${peerhost}这些占位符,实际执行时会被替换成当前连接的值。比如:

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

查出来的每一行都会被当作一条规则参与匹配。这里我给两个实操上的建议:第一,查询字段上一定要建索引,否则每条消息都全表扫描,broker 的连接数和消息量一上去数据库先扛不住;第二,把查询语句写得尽量宽容(比如允许字段为 NULL 表示通配),这样一条 SQL 就能覆盖多种规则形态,减少以后改配置的频率。

Redis 那条路更适合高并发读取的场景。EMQX 支持用 Hash 结构存规则,key 里带用户名或客户端 ID,field 是主题过滤器,value 是权限描述,形如allow:pubsub、deny:subscribe。用 Redis 的好处是查询延迟低、能扛住高频读取,配合本地缓存之后基本不会有明显的性能损耗;代价是数据结构设计要提前想清楚,而且 Redis 本身的持久化策略要配好,别把权限数据当成缓存给丢了。

3.4 三种方案怎么选:一张表说清

维度文件 acl.conf内置数据库外部数据源(MySQL/Redis)
上手成本最低低中,需准备库表和连接
动态修改需重载配置立即生效立即生效
规则承载量几十条比较合适几百到几千条上万条也没压力
与业务系统联动基本没有弱强
集群一致性需各节点同步文件自动同步天然一致
适合场景设备策略固定的小项目中小规模、要可视化运维平台级、权限与业务强关联

选型上我的经验是:别一上来就上外部数据库。很多项目评估时觉得"以后肯定要扩",结果规则表就几十条,为了这点量多维护一套数据同步链路,反而增加了故障面。先文件或者内置数据库跑起来,等到规则数确实上千、或者业务系统要求实时联动,再迁到外部数据源,迁移成本并不高。

4. 实测验证:用 MQTTX 把发布和订阅权限各打一遍

4.1 搭一个最小验证环境

规则写完一定要验证,而且要用"站在客户端角度"的方式验证,而不是看着 Dashboard 觉得对了就算完。我习惯用 MQTTX 这类图形化客户端,因为它能清楚地显示连接状态、订阅结果和收到的消息,比命令行直观。

准备三个账号来模拟不同角色:

  • dev-A001:设备账号,只该发devices/A001/up、收devices/A001/down。
  • dev-A002:另一台设备,用来验证交叉访问会被拒。
  • collector:采集端,该收devices/+/up、发devices/+/down。

先在 EMQX 里把这三个用户名对应的认证配好(认证怎么配这里不展开),然后按 3.2 的字段规则建三条授权规则。建完之后,用 MQTTX 分别建三个连接,注意每个连接的用户名和客户端 ID 都要对上——因为规则里既有按 username 匹配的,也有按 clientid 匹配的,两个值填错一个,验证结果就不可信了。

提示:客户端 ID 一定要显式设置。MQTTX 默认会生成一个随机 clientid,如果你规则里写的是{clientid, "dev-A001"}这种固定值,随机 ID 永远匹配不上,你会以为是规则写错了,其实是客户端 ID 没填对。这种情况我见过不止一次。

4.2 订阅侧验证:通配符、跨用户、共享订阅

订阅侧的验证,重点是要测"越权"和"通配符"这两类情况。

第一组,正常路径。用dev-A001订阅devices/A001/down,应该成功。然后用collector往devices/A001/down发一条消息,dev-A001应该能收到。这一步确认规则没有把该放行的挡住。

第二组,越权订阅。用dev-A001订阅devices/A002/down,应该被拒。MQTT 5.0 下会在 SUBACK 里看到失败码,MQTT 3.1.1 下看到0x80。注意:有些客户端库对订阅失败是静默处理的,界面上看起来"订阅成功了",实际服务器返回了失败码,客户端没弹提示,然后你就一直等不到消息。MQTTX 在这方面提示比较明确,所以我推荐用它做验证。

第三组,通配符越权。用dev-A001订阅devices/#,按道理应该被拒,因为它的权限只覆盖devices/A001/#。这一组是很多规则写漏的地方——如果规则表里只写了{allow, ...}而没有兜底的 deny,或者no_match还是默认的 allow,这条订阅可能就被放行了。测出来放行,说明兜底没配好。

第四组,共享订阅。如果你的系统用了$share/group/topic这种共享订阅格式,要注意 ACL 检查时对$share前缀的处理方式。不同版本的行为不完全一致,有的会把前缀剥掉再匹配主题,有的会把整个字符串拿去匹配。我实测下来,稳妥的做法是在规则里同时覆盖裸主题和带前缀的形式,别赌版本行为。这一块一定要在自己用的版本上实测一遍,别照搬别人的结论。

还有个容易忽略的点:订阅归订阅,消息归消息。订阅权限通过之后,服务器还要判断"这条消息的实际主题"是否在该客户端的订阅范围内。所以哪怕订阅成功了,如果发布方的主题写得跟你的订阅过滤器对不上,你还是收不到东西。排障的时候把这两件事分开看,能省一半时间。

4.3 发布侧验证:QoS 0 与 QoS 1 的不同表现

发布侧的验证比订阅侧更隐蔽,因为默认的deny_action = ignore会让被拒的消息悄无声息地消失。你从 MQTTX 发出去,界面显示发送成功,服务端一个字都没记,订阅方什么也收不到。

所以发布侧验证的第一步是把deny_action临时改成disconnect,让拒绝行为可见:客户端一发就被踢,说明确实被拒了。或者把日志级别调到 debug,观察授权判定的日志输出,里面会带上 clientid、username、topic、action 和最终结果,这是最准的判断依据。

具体测这几组:

  1. 用dev-A001往devices/A001/up发一条,QoS 1,应该成功。同时在collector那侧订阅devices/+/up,确认能收到。
  2. 用dev-A001往devices/A002/up发一条,QoS 1,应该被拒。MQTT 5.0 下能看到PUBACK里的0x87;MQTT 3.1.1 下可能只表现为"发了但对方没收到"。
  3. 同样的越权发布,换成 QoS 0 再发一次。QoS 0 没有回执,被拒时客户端完全无感,这是最容易误判成"服务端有 bug"的情况。如果你的业务里有 QoS 0 的越权发布,只有看服务端日志才能发现问题。
  4. 用collector往devices/A001/down发一条,应该成功;往cloud/xxx这种规则里完全没提到的主题发,看兜底策略是放行还是拒绝,用来验证no_match的实际取值。

这套测完,基本能确认规则表的方向是对的。

4.4 集群与多节点下的验证要点

单节点验证通过不等于集群验证通过,这是很多人在扩容时踩的坑。几个要点:

  • 内置数据库和外部数据源在集群里是统一的,一般不需要额外操作,规则会自动同步到各节点。但同步有延迟,加完规则立刻验证可能还没下发完,稍微等一下或者确认同步状态。
  • 文件方式的 acl.conf 不会自动同步。每个节点都有自己的配置文件,你得保证所有节点的文件内容一致,否则会出现"连到 A 节点能发,连到 B 节点被拒"这种诡异现象。
  • 验证时要覆盖到每个节点。客户端连到哪个节点是由负载均衡决定的,你只测了一台,等于没测集群。我的做法是把负载均衡临时摘掉,用每个节点的地址各连一次,跑同一组用例。
  • 缓存会影响一致性。集群环境下缓存是各节点本地的,改规则后不同节点的失效时间可能不同,短时间内出现行为不一致是正常的,别急着判定有 bug。

5. 排障实录:规则明明写了却不生效的七种原因

5.1 订阅成功但收不到消息

这是被问得最多的一类问题。原因通常不在订阅权限上,而在别的地方。按这个顺序排查会比较快:

第一,看发布的主题和订阅的过滤器是否真的匹配。sensor/+/temp匹配sensor/A/temp,但不匹配sensor/A/B/temp,也不匹配sensor/A/temp/x。这一层手动核一遍,别凭印象。

第二,看发布方有没有发布成功。如果发布方的 ACL 拒绝了这次发布,消息压根没进到 broker,订阅方当然收不到。发布和订阅是两个独立的授权判断,任何一个环节被拒,链路上都收不到消息。

第三,看 QoS 和 retained 的语义。QoS 0 的消息在订阅方掉线期间会丢,如果用 retained 消息来"补历史",得确认发布时真的设置了 retained 标志。

第四,如果用了共享订阅,确认发布方和被订阅的主题在同一个共享组逻辑下。

5.2 客户端被反复踢下线

现象是客户端连上几秒就断,日志里能看到大量断开和重连。这种基本都是deny_action = disconnect加上客户端无脑重试导致的。

要区分两种情况:一种是客户端的行为本身越权了(比如启动就订#),那得改客户端或者补规则;另一种是规则配置有误,把合法请求判成了拒绝(比如用户名占位符写错、deny 规则写得太宽),那得修规则。

排查手段很简单:把日志级别调成 debug,看每次断开前的授权判定记录,里面会明确写清楚是哪个 topic、哪个 action、被哪条规则拒了。看到这条日志,问题基本就定位了。

5.3 规则命中但结果反了

偶尔会遇到"我明明写了 allow,结果被拒了",或者反过来。常见原因是 deny 与 allow 的优先级理解错了。前面说过,deny 的优先级高于 allow,只要有一条 deny 规则命中,无论有多少 allow 命中,结果都是拒绝。

另一个原因是规则顺序导致的第一条命中即返回。虽然 deny 优先级更高,但在某些数据源实现里,第一条命中的规则就会决定结果,后面的压根不参与。所以规则表的顺序要按"从具体到宽泛"来排:最具体的规则放最上面,allow all或者deny all这种兜底放最下面。

还有一个很隐蔽的原因:多个数据源同时开启时,前一个数据源返回了"不匹配",后一个才继续查。如果你在文件里加了规则,但内置数据库里有一条宽泛的allow all排在前面生效了,那文件里的 deny 永远执行不到。排障时要先看清楚当前到底启用了哪几个数据源、顺序如何。

5.4 常见问题速查表

现象最可能的原因处理方向
规则改了不生效授权结果缓存未过期等 ttl 或临时关闭缓存
规则完全不生效用错版本语法(user / username)按大版本核对关键字
Dashboard 规则不生效占位符用了%u而不是${username}换成对应语法
订阅成功收不到消息发布侧被拒,或主题不匹配分别验证发布与订阅
客户端反复掉线deny_action = disconnect加越权请求调日志看被拒原因
换节点后行为不一致文件未同步,或缓存未失效统一文件、等待缓存过期
规则看不出问题但被拒兜底no_match或deny all命中检查兜底策略
通配符订阅被拒ACL 判断的是覆盖面而非字符串收窄订阅范围或补规则

6. 主题设计先行,ACL 才写得干净

6.1 主题命名的三条硬规矩

ACL 写得痛苦,八成是因为主题设计的时候就埋了雷。我的经验是主题设计要守三条规矩。

第一条,分层要有业务含义,别用扁平结构。devices/A001/up比A001up好写规则得多,因为前者可以在任意层级上做通配。建议的最少层次是"业务域/设备类型/设备ID/方向",比如factory/line1/dev-A001/telemetry。层次定好之后,规则里factory/line1/${clientid}/#一行就够了。

第二条,设备 ID 要能直接对应到用户名或客户端 ID。因为 ACL 的占位符只能基于 username 和 clientid 取值,如果你的主题里用的是设备序列号、数据库主键这类跟连接凭据对不上的标识,占位符就用不了,只能给每台设备写一条规则,上千台设备就是上千条规则。这个约束在设计阶段考虑进去,后面能省掉大量工作。

第三条,别让客户端有"全量订阅"的能力。#这种订阅一旦被允许,等于把整个 broker 的消息开放了。规则里应该显式拒绝它,同时业务上也要禁止客户端去订阅它。$SYS系列的主题更是要单独管控,只给运维工具开。

6.2 规则数量膨胀后怎么治理

规则一旦超过几百条,维护成本会陡增。几个实用做法:

用占位符替代重复规则。一千台设备各写一条是下策,用${clientid}一条搞定是上策。判断标准是:如果这些规则除了主体不同、其余完全一样,那就应该合并成一条带占位符的规则。

把"角色"抽象出来,而不是按人/按设备写规则。系统里真正需要的规则数量是"角色数 × 权限组合数",不是"设备数 × 权限数"。设备侧一般就两三个角色:只上报的传感器、双向的控制器、只下发的网关。把角色理清楚,规则表会立刻瘦身。

定期清理僵尸规则。设备的生命周期结束、账号停用之后,对应的规则如果没删,就会一直留在表里,既增加匹配开销,也增加审计难度。我习惯按季度过一遍规则表,跟设备台账对一次。

6.3 变更与审计

权限是安全边界,改动要走流程,别在生产 Dashboard 上随手改。我现在的习惯是:所有规则变更先在测试环境验证,再导出成配置文件或 SQL 脚本,走代码评审,最后发布。这样一来,每次改动都有记录,出问题能回滚,也能回答"这条规则是谁什么时候加的"这种问题。

审计上要关注两件事:一是被拒绝的请求量,如果某个主题的拒绝量突然飙升,可能是有人在扫主题,也可能是某台设备配置错了;二是规则表的变更记录,尤其是 deny 规则的删除和 allow 规则的新增。这两类信息配合服务端日志一起看,基本能覆盖权限侧的异常发现。

还有个小细节值得单独提一下:不要把 ACL 当成万能的隔离手段。ACL 管的是主题级别的访问控制,它管不了"同一个主题下的不同消息该给谁看"这种更细的粒度。如果你的业务有按消息内容分权的需求,得在应用层做,别指望 ACL 解决。反过来说,如果发现规则表里出现了大量基于具体主题路径的细碎规则,那大概率是主题设计出了问题,回头看看第 6.1 节那三条规矩。

关于集群环境下文件规则的同步,我还想补一句:如果你的部署方式是容器化的,最省事的做法是把 acl.conf 做成配置挂载或者打进镜像,而不是进容器里手改。手工改的文件在 Pod 重建之后会消失,而这个消失往往是在某次扩容之后才被发现,那时候你已经不记得原来写了什么了。

返回列表