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

资讯详情

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

EMQX ACL权限管控实战:MQTT主题通配符与授权配置指南

EMQX ACL权限管控实战:MQTT主题通配符与授权配置指南

1. 从"能连上"到"管得住":ACL在EMQX里到底管什么

很多人第一次把EMQX跑起来、用MQTTX连上、看到消息收发正常,就觉得这套消息系统已经"通了"。我刚开始接触MQTT那会儿也是这个心态,直到有个朋友问我一句:"你那broker是不是谁都能往任意主题发消息?"我才意识到问题的严重性——默认配置下的EMQX,只要客户端能连上,就能订阅任何主题、往任何主题发布消息。这在局域网自己玩玩没问题,一旦放到真实项目里,等于把整个消息总线的大门敞开。

ACL(Access Control List,访问控制列表)解决的就是"连上之后能干什么"这个问题。它和认证(Authentication)是两个层面的事:认证管的是"你是谁、你能不能进来",ACL管的是"进来之后,你能发布到哪些主题、能订阅哪些主题"。这个区分特别关键,我见过不少新手把两者混为一谈,结果账号密码设了一堆,权限还是乱的。

打个比方,认证像是小区大门的门禁卡,刷卡才能进小区;ACL则像是每栋楼、每个单元甚至每户的门禁,进了小区不代表你能进别人家。EMQX里这两套机制是分开配置的,认证走的是认证插件(比如用户名密码、JWT等),ACL走的是授权(Authorization)配置。默认情况下,EMQX的授权是"允许所有",也就是说认证过了之后就畅通无阻,所以我们必须主动去配ACL。

这篇笔记我打算把我在实际项目里折腾EMQX ACL的经验完整梳理一遍:它有哪些规则来源、规则怎么匹配、主题通配符怎么处理、常见坑在哪里。适合已经跑通EMQX基本收发、准备给系统加上权限管控的开发者。如果你还在"怎么安装EMQX""怎么用MQTTX连"这个阶段,建议先把基础流程走一遍再回来看,不然容易一头雾水。

2. 权限模型与规则来源:先把"谁按什么顺序说了算"搞明白

2.1 授权检查发生在哪个环节

先建立个时间线概念。一个MQTT客户端从连上到收发消息,EMQX内部大致经过这么几步:TCP连接建立、CONNECT报文认证、然后是每次PUBLISH和SUBSCRIBE时的授权检查。ACL检查是逐条消息触发的,不是连接时一次性判定。这一点很重要,意味着你改一条ACL规则,已经在线的客户端下一次发消息就会用到新规则,不需要全部断开重连(当然客户端重连最保险,下面会讲为什么)。

发布(PUBLISH)和订阅(SUBSCRIBE)这两个动作都会触发授权检查,但注意:EMQX默认对订阅的授权检查是检查订阅的主题过滤器本身。换句话说,你请求订阅sensors/#,EMQX检查的是sensors/#这个过滤器是否被允许,而不是检查它将来会匹配到的每条具体消息。这个设计直接影响你怎么写规则——如果你允许某个客户端订阅sensors/#,那它就能收到所有sensors/开头的消息,包括你不希望它看到的那部分。这也是为什么后面讲通配符时要特别小心。

2.2 三种授权数据来源

EMQX的授权规则可以来自三个地方,按优先级从高到低大致是:

来源说明适用场景
内置数据库(内置 ACL)通过Dashboard或配置文件写的静态规则中小规模、规则数量可控
文件(acl.conf)早期版本的经典方式,写到配置文件里简单固定规则、快速验证
外部数据源(HTTP/MySQL/PostgreSQL/Redis/MongoDB)规则存在外部系统,EMQX实时查询大规模、规则动态变化、与业务系统联动

我个人的使用经验是:先用内置ACL把权限跑通、验证逻辑对不对,再考虑要不要挪到外部数据源。很多小项目其实内置ACL就够了,没必要上Redis。但如果你有成千上万个设备、每个设备主题都不一样,那外部数据源几乎是必然选择,因为内置规则你手写不过来。

2.3 授权检查的匹配顺序,这是最容易踩坑的地方

EMQX的授权检查遵循一套顺序规则,理解这个顺序能省你无数排查时间。核心原则是:

  • 规则按顺序逐条匹配,一旦匹配到就立即返回结果(allow或deny),不再往后看。
  • 所有规则都没匹配到时,取决于"默认策略"(no_match),默认通常是allow,生产环境强烈建议改成deny。
  • 在某些规则类型下,deny会优先于allow生效,具体要看规则的组织方式。

注意:很多人的ACL配了半天不生效,最后发现是顺序问题——一条宽泛的allow规则写在前面,把后面的deny全盖住了。规则顺序不是随便排的,越具体的规则越要往前放,或者善用deny优先。

我踩过最典型的一个坑:给一个设备写了allow它发布到device/001/#,又写了deny它发布到device/001/control,心想"大范围允许、小范围禁止"。结果deny那条死活不生效,消息照样发出去。原因就是allow写在前面先匹配到了。解决办法是把deny规则提到allow之前,或者干脆用外部数据源的优先级机制。

2.4 授权来源的选择依据

到底用哪种来源,我总结了一个简单的判断:

  • 规则固定、数量在几十条以内、不需要运行时改:内置ACL足够。
  • 规则跟业务账号绑定、需要动态增删:上HTTP或数据库。
  • 想验证一个"是不是权限问题":临时用acl.conf或Dashboard加一条deny,最快。

这个判断不是绝对的,但能帮你快速起手。下面重点讲内置ACL和配置细节,因为这是最通用、最不影响别人系统的玩法。

3. 主题通配符与匹配规则:ACL里写主题比你想的要讲究

3.1 MQTT主题的两个通配符

在写ACL之前,必须先把MQTT主题的通配符搞透,否则规则写得再多也是错的。MQTT主题里有两个通配符:

  • +:单层通配符,匹配一层主题。比如sensor/+/temp能匹配sensor/room1/temp,但匹配不了sensor/room1/floor1/temp。
  • #:多层通配符,匹配零层或多层,只能出现在主题末尾,且必须是/后的最后一段。比如sensor/#能匹配sensor、sensor/room1、sensor/room1/temp。

关键在于:发布消息时不能带通配符,订阅时才能带通配符。所以你在ACL规则里看到通配符,基本都是在描述订阅可以覆盖的范围,或者用通配符去描述一类主题。

3.2 ACL规则里的主题怎么写才安全

ACL规则一般由几部分组成:主体(谁)、动作(发布/订阅)、主题(对哪个主题)、效果(允许/拒绝)。举几个我实际用过的例子:

  • allowusername=dev001publishdevice/dev001/up
  • denyusername=dev001publishdevice/dev001/control
  • allowusername=dev001subscribedevice/dev001/down/#

这里的第一条和第二条都是精确主题,没有通配符,这种最安全,因为范围完全可控。第三条用了#,表示dev001能订阅它自己下行的所有子主题。这种写法要保证前缀里的设备ID是唯一的,否则会出现dev001能匹配到dev0011这种滑稽事——虽然MQTT主题是分层匹配,device/dev001/down/#不会匹配device/dev0011/down/x,但在做前缀拼接的时候很容易出错,比如用字符串拼device/+ deviceId,如果deviceId本身包含/,主题层级就乱了。

提示:设备ID、租户ID这类变量在拼接成主题时,一定要做字符校验,禁止包含/、+、#这三个字符。我见过因为设备序列号里带#导致主题注入、权限绕过的案例,虽然场景特殊,但教训是真的。

3.3 用占位符让规则复用

内置ACL支持在主题里用${username}、${clientid}这类占位符,这是省事的关键。比如:

allow ${username} publish devices/${username}/data allow ${username} subscribe devices/${username}/cmd/#

这样一条规则就能覆盖所有用户,每个用户只能操作自己命名空间下的主题。但前提是用户名本身是干净的、不含特殊字符。如果你的用户名是邮箱或者带点号的字符串,虽然一般不影响主题,但让主题看起来很奇怪,建议用映射或统一的ID。

占位符还能用${clientid}、${ip}等,具体支持哪些要看EMQX版本,不同大版本之间会有差异。我建议用之前先查一下对应版本的文档,别照着旧博客抄,容易不生效。

3.4 一个容易忽略的点:订阅检查的是过滤器

前面提过一次,这里展开说。当客户端发SUBSCRIBE请求订阅a/#时,EMQX检查的是字符串a/#是否被允许,而不是展开后逐个检查。这意味着:

  • 规则里写allow subscribe a/b,客户端订阅a/#很可能就被拒了,因为a/#不等于a/b。
  • 反过来,规则里写allow subscribe a/#,客户端订阅a/#通过,那它就能收到a/下所有消息,你没法在ACL层面再细分拦截——除非用deny规则配合顺序。

所以设计主题空间时,我强烈建议提前规划好层级,让"订阅权限"和"主题范围"是一一对应的,别指望订阅一批再筛一批。这是主题设计阶段就要考虑的事,等规则写完再改,代价很大。

4. 动手配置:内置ACL从零到可用的完整流程

4.1 准备工作:确认版本和入口

先说明,不同EMQX版本(4.x和5.x)的配置界面和配置文件格式差别不小,我这里以5.x的思路为主,4.x我会在关键处标注。配置入口有两个:Dashboard的"访问控制-授权"页面,以及配置文件emqx.conf(5.x)或etc/emqx.conf+etc/acl.conf(4.x风格)。我习惯两边都看:Dashboard适合快速验证,配置文件适合版本化管理和批量下发。

动手前建议先备份当前配置,尤其是有认证插件已经在跑的环境。授权和认证虽然分开,但配置错了确实会让人一时连不上,有备份能快速回滚。

4.2 通过Dashboard添加内置ACL规则

以5.x为例,大致流程是:

  1. 登录Dashboard,进入"访问控制"→"授权"。
  2. 选择授权数据源为"内置数据库",没启用的话先启用。
  3. 添加规则,依次填:主体类型(如username)、主体值、动作(发布/订阅/全部)、主题(支持通配符和占位符)、效果(允许/拒绝)。
  4. 调整"默认策略"(no_match):生产环境设为deny或ignore,别留着allow。
  5. 保存后,规则即时生效。

这里有个实操细节:Dashboard里的规则是有顺序的,界面通常允许拖拽或按添加顺序生效。添加时想清楚deprioritize哪条。我一般按"deny在前、allow在后,具体在前、宽泛在后"的原则排列。

4.3 用配置文件批量下发

配置文件适合一次性写一整套规则。5.x里授权相关配置大致长这样(示意,字段名以你实际版本为准):

authorization { no_match = deny deny_action = ignore sources = [ { type = built_in_database enable = true } ] }

规则本身通过Dashboard或API写入内置库。4.x风格则更直接,写在acl.conf里:

{allow, {username, "dev001"}, publish, ["device/dev001/up"]}. {deny, {username, "dev001"}, publish, ["device/dev001/control"]}. {allow, {username, "dev001"}, subscribe, ["device/dev001/down/#"]}.

acl.conf的好处是直观、易读、能进Git。坏处是改完要reload或重启(部分版本支持热加载)。我早期项目就靠一个acl.conf打天下,几十条规则维护得也挺清楚。

4.4 默认策略必须改

这一条我要单独拎出来说,因为它太容易被忽略,也太危险。默认策略(no_match)出厂值是allow,意思是"所有规则都没匹配上时,放行"。如果你的规则本来就不全,等于留了个大后门。生产环境务必改成:

  • deny:没匹配到就拒绝,最安全,推荐。
  • ignore:不匹配就走别的授权源或保持中立,多源场景才用。

改完之后,一定要用真实客户端测一遍"没有规则覆盖的主题"能不能发、能不能订,确认拒绝生效了。我有次改了配置没重启成功,规则实际没生效,测了才发现,白紧张一场。

4.5 验证:用MQTTX做正反用例

配置完别急着上线,拿MQTTX这种图形客户端做正反用例最直观:

  • 正向用例:用dev001连上,往device/dev001/up发消息,应当成功;订阅device/dev001/down/#,应当成功。
  • 反向用例:用dev001往device/dev002/up发消息,应当被拒(MQTT协议层面表现为连接不断但该PUBLISH被丢弃或返回相应错误,具体表现看协议版本和配置)。
  • 边界用例:用dev001订阅device/#,根据你的规则应当被拒,验证通配符拦截有效。

注意:MQTT 3.1.1对PUBLISH被拒的处理比较"温柔",可能只是被服务端丢弃,客户端不一定收到明确错误。所以别只靠客户端提示判断,最好去EMQX日志里看授权拒绝记录,那里才是真相。

4.6 看日志确认拒绝原因

排查权限问题时,日志是第一现场。EMQX会把授权检查结果记下来,被拒时能看到是哪个clientid、哪个用户名、对哪个动作和主题被拒。把日志级别调到合适程度(别一直开debug,量大),复现问题时专看这一段。我排查过的权限问题,八成在日志里一眼就能定位,剩下两成是规则顺序或占位符拼错。

5. 常见问题排查:这些坑我基本都替你踩过了

5.1 规则写了却不生效

这是最高频的问题,原因基本跑不出这几类,我整理成速查表:

现象可能原因排查方向
规则改了没反应配置未reload/重启,或改错了文件确认生效方式,看启动日志读了哪个文件
deny规则不生效allow规则在前先匹配调整顺序,deny提前
占位符没替换版本不支持该占位符,或用户名取值不符预期查版本文档,日志打印实际取值
所有主题都被拒默认策略改deny且规则没匹配上补规则,检查主体类型是否选对
特定clientid规则无效客户端clientid动态变化(如随机后缀)改用username做主体,或固定clientid

5.2 认证和授权混淆导致的误判

"我明明加了权限怎么还能发?"——先确认你说的"加了权限"是认证还是授权。常见误区是只在认证里建了用户,就以为权限自动受限了。不是的,认证只解决"能不能连",授权才解决"能干什么"。新用户建出来,如果不配任何ACL规则、默认策略又是allow,那就是畅通无阻。

5.3 订阅通配符导致的越权

前面讲过,订阅检查的是过滤器本身。如果规则里写了allow subscribe #这种(很危险),那客户端能订任何主题。哪怕你本意是"只允许它订自己的",一旦写成#,全完了。我建议永远不要在生产规则的订阅主题里用裸#,至少加上命名空间前缀。另外,+的位置也要小心,device/+/control看起来限定了control,但+能匹配任何设备,等于允许它订所有设备的control主题,这在多租户场景里是越权。

5.4 大小写和空格问题

主题是大小写敏感的。Device/001和device/001是两个完全不同的主题,规则里大小写不一致,匹配就会失败。空格同理,很多从表格粘贴过来的规则会带上首尾空格,肉眼看不出来,机器一匹配就错。建议规则里的主题用统一的命名规范,全小写或驼峰保持一致,粘贴后检查一遍。

5.5 外部数据源的缓存延迟

如果你用了外部数据源(比如Redis或HTTP),改完规则不是立即生效的,因为EMQX有缓存(有些版本默认缓存时间不短)。表现就是"我数据库都改了,怎么还没生效"。解决办法是调小缓存时间(会增加查询压力),或者手动清缓存/重启授权源,或者干脆在测试阶段用内置ACL验证逻辑。这个坑我在上外部授权时吃过,一度以为是代码bug,查了半天才发现是缓存。

5.6 客户端重连与权限更新

规则改了之后,已连接的客户端在下一次动作时会用新规则,这点是好的。但有些SDK会把订阅状态缓存在本地,你以为它断了,其实还连着旧会话。所以变更权限后,稳妥做法是让相关客户端重连一次,或者用会话清理机制(clean session相关配置)确保状态一致。

6. 从能用走向好用:ACL设计的几个经验教训

6.1 主题命名规范是权限设计的地基

我现在做任何MQTT项目,第一步不是写ACL,而是定主题规范。一个我常用的结构是:业务域/租户或设备ID/方向/具体功能。比如iot/tenantA/dev001/up和iot/tenantA/dev001/down/cmd。有了这个结构,ACL规则就能很自然地写成基于前缀的allow,租户之间天然隔离,设备之间也隔离。反过来,如果主题是随手起的,ACL只能一条条硬写,维护成本爆炸。

6.2 最小权限不是口号,是省事

"给这个设备开大点权限,免得后面不够用"——这种想法害人不浅。权限开大了,第一不安全,第二以后想收紧时你得先搞清楚现有的消息流依赖了哪些主题,改起来牵一发动全身。我的做法是先给最窄的权限,跑不通再加,宁可多改几次规则,也别一次性放太宽。这个顺序反过来做,基本没有回头路。

6.3 规则的可维护性

规则多了以后,可读性直接决定维护效率。我的习惯:

  • 按业务域或设备类型分组,加注释说明每组规则干嘛的。
  • 主体值用统一的变量(用户名、clientid)而不是硬编码一堆具体值。
  • 定期清理不再使用的规则,别让它变成一坨没人敢动的东西。

用外部数据源时,把规则存成结构化的表,配上变更记录,比散落在配置文件里强得多。

6.4 压测与权限的相互作用

有个容易被忽略的点:ACL检查是有成本的。规则特别多、又走了外部查询(每次都要问一次数据库/HTTP),在压测高并发时会成为瓶颈。我遇到过加了外部ACL后QPS明显下降的情况,后来通过加缓存、精简规则、把常用规则挪到内置库解决了。所以ACL不是配完就完事,还得关注它对吞吐的影响,尤其是设备规模上来之后。

6.5 安全审计与日志留存

生产环境里,授权拒绝的日志值得留存和定期看。它不仅能帮你发现配置错误,还能暴露异常行为——比如某个设备突然开始尝试访问不属于它的主题,这往往意味着设备被入侵或程序有bug。我一般会把这类日志集中收集,设个简单的告警。这个习惯帮我提前发现过好几次异常访问尝试,事后看都是没配好导致的误伤,但宁可信其有。

最后分享一个我自己的操作习惯:每次改ACL,我都会先在测试环境用MQTTX把正反用例跑一遍,再上生产,而且改完必看一小段日志确认拒绝和放行都符合预期。权限这东西,配错了往往没有明显报错,等发现时可能已经出事了。花五分钟验证,比事后排查半天划算得多。

返回列表