1. 为什么你需要更精细的物联网设备权限管理?
大家好,我是老张,在物联网这个圈子里摸爬滚打了十来年,经手过不少智能家居、工业传感的项目。我猜你可能遇到过这样的头疼事:成百上千的设备接入了你的MQTT服务器(比如EMQX),结果某个设备“抽风”,往不该发的主题里狂发数据,或者偷听了其他设备的指令,整个系统乱成一锅粥。说到底,这就是权限没管好。
EMQX自带的默认ACL(访问控制列表)配置文件(acl.conf)对付小场景还行,一旦设备量上来,规则复杂了,手动改文件、重启服务简直就是噩梦。你想想,每增加一个设备类型,都要去服务器上改配置,这运维成本谁受得了?所以,把权限规则从静态文件里“搬”出来,放到像MySQL这样的数据库里,就成了一个自然而然的选择。这不仅仅是换个地方存数据,而是实现动态、精细、可集中管理的设备级权限控制。
简单来说,EMQX结合MySQL做ACL,核心目标就一个:让每台设备(每个客户端)都能被清晰地定义——谁能发布(Publish)什么消息到哪个主题(Topic),谁能订阅(Subscribe)哪个主题的消息。比如,你家的智能窗帘控制器,只能订阅/home/livingroom/curtain/set来接收指令,只能发布/home/livingroom/curtain/status来上报状态;而客厅的温湿度传感器,则只能发布/home/livingroom/sensor/temperature和/home/livingroom/sensor/humidity。它们之间不能互相越界,这样系统才安全、清晰。
接下来,我就手把手带你,从零开始,把EMQX和MySQL“撮合”到一起,搭建一套能在生产环境扛得住、管得细的权限控制系统。咱们不搞理论,直接上干货。
2. 前期准备:让你的EMQX和MySQL“认识”一下
万事开头难,但准备工作做扎实了,后面就顺了。这里我假设你已经有了EMQX和MySQL的基本运行环境。如果还没装,EMQX官网有各种系统的安装包,MySQL用5.7或8.0版本都行,安装过程网上教程很多,我就不赘述了。
2.1 搞定EMQX的基础安全配置
在请出MySQL这位“外援”之前,咱们得先把EMQX自家的“门”关好。默认情况下,EMQX是允许匿名连接的,这相当于大门敞开,太危险了。
首先,找到你的EMQX配置文件 emqx.conf。它的路径通常在 /etc/emqx/ 或者你安装目录下的 etc/ 文件夹里。用你喜欢的编辑器打开它,找到关于匿名认证的那一行:
## 找到这一行,通常在第100行左右
allow_anonymous = true
务必把它改成 false:
allow_anonymous = false
这个改动意味着,从此以后,任何客户端连接EMQX,都必须提供用户名和密码(或其他认证方式)进行“验明正身”,否则连大门都进不来。这是所有权限控制的第一道防火墙。
接着,处理一下默认的ACL文件。EMQX自带一个 acl.conf 文件,里面有一些基础规则。在我们启用MySQL ACL插件后,EMQX会优先查询数据库中的规则,如果数据库里没找到匹配的规则,才会回退到这个文件里的规则。为了不产生混淆,我建议你先清理一下这个文件。找到 etc/acl.conf,将其内容修改为类似下面的保守策略:
%% 只允许仪表盘用户订阅系统主题
{allow, {user, "dashboard"}, subscribe, ["$SYS/#"]}.
%% 允许本机IP进行所有操作(方便本地管理调试)
{allow, {ipaddr, "127.0.0.1"}, pubsub, ["$SYS/#", "#"]}.
%% 默认拒绝所有客户端订阅系统主题和根主题(这是一个非常安全的默认策略)
{deny, all, subscribe, ["$SYS/#", {eq, "#"}]}.
%% 注意:最后一行全局允许 {allow, all}. 已经被我们删除了!
这个配置表达了一个“白名单”思想:默认情况下,权限是收紧的。除非在MySQL的规则表里明确允许,否则很多操作会被拒绝。这符合安全上的“最小权限原则”。
2.2 配置MySQL插件,建立通信桥梁
现在,轮到主角之一登场了:emqx_auth_mysql 插件。这个插件就是EMQX和MySQL之间的“翻译官”和“信使”。
找到插件的配置文件,路径通常是 etc/plugins/emqx_auth_mysql.conf。我们需要配置MySQL数据库的连接信息。下面是一个示例,你需要根据自己实际的数据库情况来修改:
## MySQL服务器地址和端口。如果MySQL和EMQX在同一台机器,就用127.0.0.1:3306
auth.mysql.server = 192.168.1.100:3306
## 数据库连接池大小,根据你的客户端并发数调整,一般8-32够用
auth.mysql.pool = 8
## 连接MySQL的用户名和密码
auth.mysql.username = emqx_user
auth.mysql.password = YourStrongPassword123!
## 要使用的数据库名,你需要提前创建好这个数据库
auth.mysql.database = iot_emqx_acl
## 查询超时时间
auth.mysql.query_timeout = 5s
## 密码哈希方式。这里先设为‘plain’,即不加密,方便初次测试。
## **生产环境务必改为sha256、bcrypt等强哈希方式!**
auth.mysql.password_hash = plain
这里我特别想强调两点,都是踩坑踩出来的经验:
- 数据库用户权限:不要直接用
root用户!专门为EMQX创建一个用户(比如emqx_user),只授予它对iot_emqx_acl数据库必要的读写权限。这能有效降低数据库安全风险。 - 密码哈希:
plain(明文)只是为了测试方便。真实场景下,客户端密码绝对不能明文存储在数据库里。改成sha256或bcrypt后,EMQX插件在认证时,会先将客户端传来的密码用同样算法哈希,再与数据库存储的哈希值比对。我等下会在建表时给你展示对应的SQL该怎么写。
3. 设计数据库:权限规则的“宪法”存储地
数据库表设计得好不好,直接决定了你后期权限管理的灵活性和复杂度。EMQX插件有默认的表结构,但我们完全可以,也建议你,根据业务需求进行微调。
3.1 创建用户认证表(mqtt_user)
这张表用来做客户端的身份认证(Authentication),解决“你是谁”的问题。
CREATE TABLE `mqtt_user` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(100) DEFAULT NULL COMMENT 'MQTT用户名',
`password_hash` varchar(200) DEFAULT NULL COMMENT '密码哈希值',
`salt` varchar(40) DEFAULT NULL COMMENT '密码盐值(用于bcrypt等算法)',
`is_superuser` tinyint(1) DEFAULT 0 COMMENT '是否是超级用户:0否,1是',
`description` varchar(200) DEFAULT NULL COMMENT '用户描述',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `unique_username` (`username`), -- 用户名必须唯一
INDEX `idx_username` (`username`) -- 为查询建立索引
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MQTT用户认证表';
和默认表结构的主要区别与考量:
- 我把字段名从
password改成了password_hash,目的就是提醒你,这里面存的是哈希值,不是明文。 - 增加了
description字段,方便后期维护,知道这个账号是给哪类设备用的。 - 增加了
created_at字段,便于审计。 - 为
username建立了唯一索引和普通索引,加快认证时的查询速度。
插入示例数据:
假设我们使用 sha256 哈希,并且不加盐(salt为空)。你可以用在线工具或命令行先算出密码的哈希值。例如,密码 device_secure_pass 的SHA256哈希值是 a1b2c3...(这里是一个简写,实际是一长串16进制字符)。
-- 插入一个普通设备用户
INSERT INTO `mqtt_user` (`username`, `password_hash`, `is_superuser`, `description`)
VALUES (
'temperature_sensor_01',
'a1b2c3...(这里是完整的sha256哈希值)',
0,
'客厅温度传感器'
);
-- 插入一个超级用户(可用于管理后台等,通常拥有所有权限)
INSERT INTO `mqtt_user` (`username`, `password_hash`, `is_superuser`, `description`)
VALUES (
'admin',
'x9y8z7...(另一个哈希值)',
1,
'系统管理员'
);
超级用户(is_superuser) 是个很关键的角色。当插件启用后,如果一个用户被标记为超级用户,那么ACL规则将不会对它进行校验,它默认拥有对所有主题的发布和订阅权限。这个功能要慎用,通常只给极少数管理型客户端。
3.2 创建ACL规则表(mqtt_acl)
这是核心中的核心,解决了“你能干什么”的问题。每一条记录都是一条宪法级别的“法令”。
CREATE TABLE `mqtt_acl` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`allow` tinyint(1) NOT NULL DEFAULT 1 COMMENT '操作权限:0拒绝,1允许',
`username` varchar(100) DEFAULT NULL COMMENT '匹配用户名,$all代表所有用户',
`clientid` varchar(100) DEFAULT NULL COMMENT '匹配客户端ID',
`ipaddr` varchar(60) DEFAULT NULL COMMENT '匹配客户端IP地址(支持CIDR格式,如192.168.1.0/24)',
`access` tinyint(2) NOT NULL COMMENT '操作类型:1订阅,2发布,3发布和订阅',
`topic` varchar(255) NOT NULL COMMENT '主题过滤器,支持通配符和占位符',
`priority` int(11) DEFAULT 10 COMMENT '规则优先级,数字越小优先级越高',
`comment` varchar(200) DEFAULT NULL COMMENT '规则说明',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `idx_username` (`username`),
INDEX `idx_clientid` (`clientid`),
INDEX `idx_ipaddr` (`ipaddr`),
INDEX `idx_access_topic` (`access`, `topic`(20)) -- 联合索引,提升查询效率
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MQTT访问控制规则表';
我对表结构的增强解读:
allow字段:这是规则的“判决结果”。1代表允许,0代表拒绝。EMQX在匹配规则时,一旦找到一条allow=0的拒绝规则,就会立即拒绝请求;而允许规则则需要找到匹配的才会通过。- 匹配条件(
username,clientid,ipaddr):这三个字段是 “且” 的关系。如果一条规则同时指定了username和clientid,那么只有当客户端的用户名和客户端ID同时匹配时,这条规则才生效。如果某个字段为NULL,则表示忽略这个条件的匹配。特别地,username字段可以设置为‘$all’,表示这条规则适用于所有用户。 access字段:精准控制操作类型。1(Subscribe)只控制订阅,2(Publish)只控制发布,3(Pubsub)两者都控制。我强烈建议你尽量使用1或2,实现最小权限控制,而不是图省事都用3。topic字段:这是规则生效的“范围”。支持MQTT通配符:+(单层通配)和#(多层通配)。还支持占位符%u(用户名)和%c(客户端ID),用于实现动态主题,这是实现设备级隔离的关键。- 我新增的
priority字段:这是实战中非常重要的一个优化!当多条规则都可能匹配同一个客户端请求时,EMQX默认的处理顺序可能不符合你的预期。通过这个字段,我们可以显式地定义规则的优先级。比如,一条拒绝某个IP段访问特定主题的规则(优先级设为1),其优先级应该高于一条允许所有用户访问某个通用主题的规则(优先级设为99)。EMQX插件需要稍作修改来支持按优先级排序,但更常见的做法是在查询时用ORDER BY priority ASC来获取规则,确保先匹配高优先级(数字小)的规则。 comment字段:加一条注释,三个月后你回来维护时,会感谢自己的。
4. 实战:编写你的第一条精细化ACL规则
光说不练假把式,我们现在就往 mqtt_acl 表里插入几条有代表性的规则,感受一下它的威力。假设我们有一个智能家居场景,有温度传感器、灯光控制器和一个家庭网关。
4.1 基础规则:隔离设备类型
首先,我们禁止所有设备瞎订阅系统主题和根主题,这是一个安全基线。
-- 规则1:禁止所有用户订阅系统主题(高优先级安全规则)
INSERT INTO mqtt_acl (allow, username, access, topic, priority, comment)
VALUES (0, '$all', 1, '$SYS/#', 1, '禁止所有客户端订阅系统主题');
-- 规则2:禁止所有用户订阅根主题‘#’(防止数据泛滥)
INSERT INTO mqtt_acl (allow, username, access, topic, priority, comment)
VALUES (0, '$all', 1, '#', 5, '禁止所有客户端全局订阅');
4.2 设备级发布/订阅隔离
现在,为具体的设备创建规则。假设我们的客户端连接时,username 就是设备编号,如 sensor_temp_01,clientid 可以与之相同或不同。
-- 规则3:允许温度传感器 sensor_temp_01 发布其专属主题
INSERT INTO mqtt_acl (allow, username, clientid, access, topic, comment)
VALUES (1, 'sensor_temp_01', NULL, 2, '/home/livingroom/sensor/temperature', '客厅温度传感器上报数据');
-- 规则4:允许灯光控制器 light_switch_01 订阅其控制主题,并发布状态主题
-- 它需要订阅指令,并发布自己的状态
INSERT INTO mqtt_acl (allow, username, access, topic, comment)
VALUES (1, 'light_switch_01', 1, '/home/livingroom/light/switch/set', '订阅灯光开关指令');
INSERT INTO mqtt_acl (allow, username, access, topic, comment)
VALUES (1, 'light_switch_01', 2, '/home/livingroom/light/switch/status', '发布灯光开关状态');
4.3 使用通配符和占位符实现动态主题
这是实现大规模设备权限隔离的“银弹”。我们不想为成百上千个相同的设备每台都写一条规则。
-- 规则5:允许任何用户发布消息到以自己用户名命名的状态主题(动态主题)
-- 占位符 %u 会在匹配时被替换为实际的用户名
INSERT INTO mqtt_acl (allow, username, access, topic, comment)
VALUES (1, '$all', 2, '/device/status/%u', '允许设备上报自身状态到专属主题');
-- 规则6:允许家庭网关订阅所有设备的状态主题
-- 网关的 username 是 ‘home_gateway’
INSERT INTO mqtt_acl (allow, username, access, topic, comment)
VALUES (1, 'home_gateway', 1, '/device/status/+', '允许网关订阅所有设备状态');
这条规则的效果是:
- 设备
sensor_temp_01可以发布消息到/device/status/sensor_temp_01。 - 设备
light_switch_01可以发布消息到/device/status/light_switch_01。 - 而
home_gateway因为订阅了/device/status/+,它能收到所有设备状态主题的消息。 - 但是,
sensor_temp_01无法发布到/device/status/light_switch_01,因为主题不匹配。这就实现了设备间的数据隔离,同时又让网关能汇聚所有数据。
4.4 基于IP段的规则
你还可以根据设备的网络位置来制定规则,比如只允许来自内网特定网段的设备发布控制指令。
-- 规则7:只允许来自192.168.1.0/24网段的客户端向指令主题发布消息
INSERT INTO mqtt_acl (allow, ipaddr, access, topic, comment)
VALUES (1, '192.168.1.0/24', 2, '/control/+/command', '只允许内网网段发送控制指令');
-- 规则8:拒绝来自特定IP(10.0.0.100)的客户端订阅任何视频流主题
INSERT INTO mqtt_acl (allow, ipaddr, access, topic, priority, comment)
VALUES (0, '10.0.0.100', 1, '/camera/+/stream/#', 2, '禁止特定IP订阅视频流');
5. 启动、测试与排坑指南
规则都写好了,是时候让整个系统跑起来,看看效果了。
5.1 启动插件与验证
-
启动EMQX服务(如果还没启动的话):
./bin/emqx start -
加载MySQL认证插件:
./bin/emqx_ctl plugins load emqx_auth_mysql如果看到
Plugin emqx_auth_mysql loaded successfully.的提示,说明插件加载成功。你也可以用./bin/emqx_ctl plugins list查看插件状态,确认emqx_auth_mysql是active的。踩坑提示:如果这里报错,最常见的原因是数据库连接不上。请回头检查
emqx_auth_mysql.conf中的数据库地址、端口、用户名、密码、数据库名是否正确,以及MySQL服务是否运行,防火墙端口(3306)是否开放。 -
测试连接: 我强烈推荐使用
MQTTX这款图形化客户端工具进行测试,它非常直观。- 测试1(匿名连接失败):不填写用户名和密码,尝试连接。应该会立即被拒绝,提示“Not authorized”或“Bad username or password”。这说明我们第一步关闭匿名认证成功了。
- 测试2(正确用户连接成功):使用
mqtt_user表中存在的用户名和密码(例如sensor_temp_01和对应的密码)进行连接。应该能成功连接。 - 测试3(发布/订阅权限验证):连接成功后,进行具体的发布订阅测试。
- 用
sensor_temp_01客户端尝试发布到/home/livingroom/sensor/temperature,应该成功。 - 尝试用同一个客户端订阅
/home/livingroom/light/switch/set,应该被拒绝(因为我们只给了它发布权限,没给订阅权限)。 - 用
home_gateway客户端尝试订阅/device/status/+,应该成功。 - 用
sensor_temp_01尝试发布到/device/status/light_switch_01,应该失败(因为动态主题%u只匹配自己的用户名)。
- 用
5.2 生产环境进阶考量
当你把这套方案用到真实项目中时,还有几个点需要特别注意:
- 性能与缓存:EMQX的MySQL插件默认会缓存ACL查询结果。缓存的TTL(生存时间)可以在配置文件中调整(
auth.mysql.acl_query.cache相关配置)。对于规则变动不频繁的场景,适当延长缓存时间可以大幅减少数据库查询压力。但如果你的权限规则需要动态实时更新,可能需要设置较短的缓存时间,甚至考虑手动调用API清除缓存。 - 规则匹配顺序与优化:正如前面提到的,当规则很多时,匹配顺序至关重要。除了用
priority字段,还要注意规则的特异性。应该把最具体、范围最小的规则(比如拒绝某个具体设备)放在前面,把最通用的规则(比如允许某个网段)放在后面。确保你的SQL查询语句是加了索引并按优先级排序的,例如:
(注意:插件内部查询是固定的,你需要根据插件源码或自定义查询来优化,这里只是示意逻辑)SELECT allow, access, topic FROM mqtt_acl WHERE (username = ? OR username = '$all' OR username IS NULL) AND (clientid = ? OR clientid IS NULL) AND (ipaddr = ? OR ipaddr IS NULL) ORDER BY priority ASC, id ASC; - 与认证分离:本文为了讲解方便,将认证(
mqtt_user)和授权(mqtt_acl)都放在了MySQL。在实际大型系统中,你可能希望将它们解耦。例如,使用Redis做快速的用户认证(因为认证发生在每次连接时,频率高),而用MySQL管理复杂的ACL规则(因为规则查询发生在发布/订阅时,且可缓存)。EMQX支持同时启用多个认证/ACL插件,你可以灵活搭配。 - 监控与审计:定期检查
mqtt_acl表的规则,清理过时或无效的条目。可以在EMQX中启用日志,关注ACL拒绝(Deny)的日志,这可能是设备配置错误或攻击尝试的迹象。
这套基于MySQL的精细化ACL方案,我从早期的EMQX 3.x版本就开始用,一直迭代到现在5.x版本,核心思想没变,就是让权限控制变得动态、可管理。它可能不是性能极限最高的方案(对于超大规模、规则极简的场景,静态文件或内置模块可能更快),但在灵活性、可维护性和功能强大性上取得了非常好的平衡。刚开始配置可能会觉得有点繁琐,但一旦跑通,你会发现在管理成千上万的设备权限时,这种“数据库驱动”的方式是多么的省心和强大。
1029



被折叠的 条评论
为什么被折叠?



