- 物联网
- 消息队列
- 后端
【免费下载链接】mosquitto
Eclipse Mosquitto - An open source MQTT broker
本文基于 Eclipse Mosquitto 仓库中发布于 2010 年的官方博文 mqtt-v3-1.md,梳理 MQTT v3.1 规范相对 v3.0 的核心变化——在 CONNECT 报文中加入可选的用户名与密码字段,并结合当前仓库源码展示 Mosquitto 如何实现、校验与配置这一认证能力。读完本文,你将掌握 v3.1/v3.1.1 在 Mosquitto 客户端与 Broker 两侧的完整报文构造与解析链路,以及如何通过password_file、allow_anonymous与 ACL 文件实现按用户名控制 Broker 与主题访问。
一、背景:MQTT v3.1 的诞生与关键变化
MQTT v3 规范随后被更新为 v3.1。这次更新的最显著变化,是在 CONNECT(连接)命令中加入了发送**用户名(username)和密码(password)**的可选项。也就是说,从 v3.1 开始,MQTT 协议在应用层原生支持了最基本的身份认证手段,客户端可以在建立连接时向 Broker 声明自己的身份。
这一点在当时的背景尤为重要:v3.1 规范本身比原始版本"可读性更强、更清晰",同时 Mosquitto 官方在 2010 年发布该博文时明确表示,Mosquitto 将在未来版本中支持 v3.1 规范,并支持按用户名控制 Broker 与主题访问权限。若用户当时急需该能力,可暂用 IBM 的 RSMB 代理做测试(其包内同样包含 MQTT 客户端库、简易发布客户端与订阅客户端,但需自行核对许可证条款)。
从今天的仓库视角回看,这一承诺已完整落地:当前 Mosquitto 同时支持 MQTT v3.1、v3.1.1 与 v5 三种协议版本,用户名密码认证、ACL 主题访问控制均已成为内置安全模块的核心能力。
二、协议层面的标识:v3.1 与 v3.1.1 的字节级差异
要理解 Mosquitto 如何区分协议版本,可以先看协议常量定义文件 include/mosquitto/mqtt_protocol.h:
#define PROTOCOL_NAME_v31 "MQIsdp" /* v3.1 使用的协议名 */ #define PROTOCOL_VERSION_v31 3 #define PROTOCOL_NAME "MQTT" /* v3.1.1 与 v5 使用的协议名 */ #define PROTOCOL_VERSION_v311 4 #define PROTOCOL_VERSION_v5 5这里揭示了两个关键事实:
- v3.1 的 CONNECT 报文中,协议名(Protocol Name)字段写的是
"MQIsdp"(即 MQ Is dp,MQ 消息分发协议的遗留命名),版本号(Protocol Level)为3; - v3.1.1 与 v5的协议名统一为
"MQTT",版本号分别为4与5。
对应地,客户端库选项在 include/mosquitto/defs.h 中定义为:
#define MQTT_PROTOCOL_V31 3 #define MQTT_PROTOCOL_V311 4三、客户端侧实现:CONNECT 报文的构造与用户名密码写入
3.1 报文构造主流程
客户端发送 CONNECT 的核心实现在 lib/send_connect.c 的send__connect()函数中。它根据mosq->protocol选择版本,并计算不同的可变报头长度:
}else if(mosq->protocol == mosq_p_mqtt311){ version = MQTT_PROTOCOL_V311; headerlen = 10; }else if(mosq->protocol == mosq_p_mqtt31){ version = MQTT_PROTOCOL_V31; headerlen = 12; /* v3.1 的协议名字符串比 v3.1.1 长 2 字节 */ }紧接着写入协议名与版本号:
if(version == MQTT_PROTOCOL_V31){ packet__write_string(packet, PROTOCOL_NAME_v31, (uint16_t)strlen(PROTOCOL_NAME_v31)); }else{ packet__write_string(packet, PROTOCOL_NAME, (uint16_t)strlen(PROTOCOL_NAME)); } packet__write_byte(packet, version);3.2 连接标志位:用户名/密码位如何被置位
v3.1/v3.1.1 的 CONNECT 报文第 8 字节是"连接标志(Connect Flags)",其中 bit 7 为用户名标志(Username Flag),bit 6 为密码标志(Password Flag)。send__connect()中的对应逻辑为:
byte = (uint8_t)((clean_session&0x1)<<1); if(will){ byte = byte | (uint8_t)(((mosq->will->msg.qos&0x3)<<3) | ((will&0x1)<<2)); ... } if(username){ byte = byte | 0x1<<7; /* 设置 Username Flag */ } if(mosq->password){ byte = byte | 0x1<<6; /* 设置 Password Flag */ } packet__write_byte(packet, byte);可见用户名与密码标志是相互独立置位的:客户端可以只带用户名(典型场景),也可以用户名密码同时携带;而密码必须依赖用户名。
3.3 一个容易踩的坑:只有密码没有用户名
send__connect()在组装报文前做了前置校验:
if(mosq->protocol == mosq_p_mqtt31 || mosq->protocol == mosq_p_mqtt311){ if(password != NULL && username == NULL){ return MOSQ_ERR_INVAL; /* 拒绝构造:v3.1/v3.1.1 不允许只有密码没有用户名 */ } }也就是说,在 v3.1 与 v3.1.1 中,如果只设置了密码而未设置用户名,mosquitto_connect()系列调用会直接返回MOSQ_ERR_INVAL,因为按规范密码标志位只有在用户名标志位为 1 时才有意义。
3.4 负载(Payload)的写入顺序
用户名与密码写入 CONNECT 报文体(payload)的末尾,紧随客户端 ID 与遗嘱消息(Will)之后:
/* Payload */ if(clientid){ packet__write_string(packet, clientid, (uint16_t)strlen(clientid)); }else{ packet__write_uint16(packet, 0); } if(will){ packet__write_string(packet, mosq->will->msg.topic, ...); packet__write_string(packet, (const char *)mosq->will->msg.payload, ...); } if(username){ packet__write_string(packet, username, (uint16_t)strlen(username)); } if(password){ packet__write_string(packet, password, (uint16_t)strlen(password)); }负载中每个字符串都遵循"2 字节长度 + 内容"的编码方式(packet__write_string),这也正是 v3.1 规范对 UTF-8 字符串字段的统一要求。
四、Broker 侧实现:CONNECT 报文的解析与版本校验
4.1 版本号检查与协议分发
Broker 端解析 CONNECT 的逻辑位于 src/handle_connect.c。它对协议版本号做了严格校验:
if((protocol_version&0x7F) != PROTOCOL_VERSION_v31){ /* v3.1 必须携带版本号 3(可带 0x80 私用标志位,故用 &0x7F 屏蔽) */ ... } if((protocol_version&0x7F) == PROTOCOL_VERSION_v311){ ... }从这段代码可以推断:Broker 允许 v3.1 的版本字节带有最高位(bit 7)的附加标志(这一机制后来被桥接模式的try_private私用扩展使用,见 lib/send_connect.c 中version |= 0x80的桥接分支),因此校验时统一用&0x7F取出低 7 位再比较。
4.2 认证通过后的接入流程
当用户名密码通过校验后,Broker 进入connect__on_authorised()完成后续接入:
- 检查同名客户端是否已在线,若在线则执行"会话接管(session taken over)"逻辑,向旧连接发送 DISCONNECT 并断开;
- 处理
clean_session/clean_start与持久会话的订阅、Inflight 消息迁移; - 记录连接日志,例如
New client connected from %s:%d as %s (p%d, c%d, k%d, u'%s')(u'...'即登录用户名,见 src/handle_connect.c); - 按
keepalive、max_qos等策略初始化连接后,发送CONNACK_ACCEPTED。
4.3 认证失败时的 CONNACK 返回码
v3.1/v3.1.1 共用的 CONNACK 返回码定义在 include/mosquitto/mqtt_protocol.h:
enum mqtt311_connack_codes { CONNACK_ACCEPTED = 0, CONNACK_REFUSED_PROTOCOL_VERSION = 1, CONNACK_REFUSED_IDENTIFIER_REJECTED = 2, CONNACK_REFUSED_SERVER_UNAVAILABLE = 3, CONNACK_REFUSED_BAD_USERNAME_PASSWORD = 4, /* 用户名或密码错误 */ CONNACK_REFUSED_NOT_AUTHORIZED = 5, };其中CONNACK_REFUSED_BAD_USERNAME_PASSWORD(值为 4)正是为 v3.1 引入用户名密码认证而对应的拒绝码——客户端携带错误的用户名/密码时,Broker 会以此返回码拒绝连接。
五、让 v3.1 认证真正生效:密码文件与访问控制配置
v3.1 引入的"用户名控制 Broker 与主题访问"能力,在 Mosquitto 中落地为内置安全模块(builtin-security)。相关配置项的解析在 src/conf.c 中,安全数据的加载在 src/security_default.c 中完成。
5.1password_file:指定用户名密码文件
# mosquitto.conf password_file /etc/mosquitto/pwfile配置解析位于 src/conf.c:该选项把文件路径存入security_options->password_file。Broker 启动时,mosquitto_security_init_default()会调用unpwd__file_parse()解析该文件,并注册MOSQ_EVT_BASIC_AUTH回调(src/security_default.c)。若启用了per_listener_settings true,每个 listener 可拥有各自的password_file,且必须将其置于其他安全配置项之前。
密码文件的格式为每行一个条目:
username:hashed-password仓库根目录的 pwfile.example 给出了真实示例(密码以$6$开头的 SHA-512 加盐哈希存储):
roger:$6$clQ4Ocu312S0qWgl$Cv2wUxgEN73c6C6jlBkswqR4AkHsvDLWvtEXZZ8NpsBLgP1WAo/qA+WXcmEN/mjDNgdUwcxRAveqNMs2xUVQYA== sub_client:$6$U+qg0/32F0g2Fh+n$fBPSkq/rfNyEQ/TkEjRgwGTTVBpvNhKSyGShovH9KHewsvJ731tD5Zx26IHhR5RYCICt0L9qBW0/KK31UkCliw== pub_client:$6$vxQ89y+7WrsnL2yn$fSPMmEZn9TSrC8s/jaPmxJ9NijWpkP2e7bMJLz78JXR1vW2x8+T3FZ23byJA6xs5Mt+LeOybAHwcUv0OCl40rA==5.2mosquitto_passwd:生成与维护密码文件
推荐使用官方工具生成上述文件,其用法(见 apps/mosquitto_passwd/mosquitto_passwd.c):
Usage: mosquitto_passwd [-H argon2 | -H sha512-pbkdf2] [-c | -D] passwordfile username mosquitto_passwd [-H argon2 | -H sha512-pbkdf2] [-c] -b passwordfile username password mosquitto_passwd -U passwordfile常用操作示例:
# 新建密码文件并添加用户(-c 表示创建/覆盖文件) mosquitto_passwd -c /etc/mosquitto/pwfile roger # 批量模式:直接以命令行参数指定密码(适合脚本) mosquitto_passwd -b /etc/mosquitto/pwfile sub_client sub_password # 删除用户(-D) mosquitto_passwd -D /etc/mosquitto/pwfile roger # 将旧格式密码文件升级为新的哈希格式(-U) mosquitto_passwd -U /etc/mosquitto/pwfile5.3allow_anonymous:是否放行匿名连接
设置password_file后,还需注意匿名策略。在 src/conf.c 中有如下推断逻辑:一旦配置了认证/访问控制类选项(如password_file或acl_file),且未显式设置allow_anonymous,则该值被自动置为false——即"配了密码文件就默认禁止匿名登录"。
显式配置方式:
allow_anonymous false在 src/security_default.c 的认证流程中,当allow_anonymous为false且客户端未提供用户名时,匿名连接会被直接拒绝;反之若allow_anonymous true,匿名客户端仍可连接,但只能访问 ACL 允许的主题。
5.4acl_file:按用户名控制主题访问
"按用户名控制主题访问"由 ACL 文件实现:
# mosquitto.conf acl_file /etc/mosquitto/aclfile仓库根目录的 aclfile.example 提供了模板,典型结构为:
# 授予特定用户主题访问权限 user roger topic read/write sensor/# # roger 可读写 sensor/# 主题 topic read $SYS/# # roger 可读 $SYS/# 系统主题 # 对未匹配任何 user 段的客户端生效 topic read $SYS/#ACL 文件在 Broker 启动时由aclfile__parse()解析,并注册MOSQ_EVT_ACL_CHECK回调(src/security_default.c)。每次 PUBLISH 与 SUBSCRIBE 都会经过该回调,按"客户端用户名 → 匹配的 user 段 → topic 模式"的顺序做权限裁决,从而实现真正意义上的"按用户名控制主题访问"。
5.5 从命令行指定协议版本
客户端侧可通过-V参数选择协议版本(client/client_shared.c):
# 使用 MQTT v3.1(协议名 MQIsdp,版本号 3) mosquitto_pub -V mqttv31 -u roger -P password -t sensor/temp -m 23.5 # 使用 MQTT v3.1.1(默认) mosquitto_sub -V mqttv311 -u roger -P password -t sensor/#-u/-P分别指定用户名与密码;当仅指定-u时,客户端只发送用户名而不发送密码(连接标志只置 Username Flag)。工具mosquitto_ctrl同样支持-V切换协议(见 apps/mosquitto_ctrl/options.c)。
六、从 v3.1 到 v3.1.1 的演进脉络
v3.1 引入用户名密码字段后,MQTT 协议认证能力得以标准化,但也留下了一些歧义(如客户端 ID 为空时的行为、遗嘱消息标志的位序等)。随后发布的 v3.1.1(即 OASIS 标准 MQTT 3.1.1)在保留用户名/密码字段设计的同时,将协议名统一为"MQTT"、版本号升为4,并大幅收紧规范措辞。
Mosquitto 对两者的支持是完全并存的:
- 客户端库默认使用
MQTT_PROTOCOL_V311(见 include/mosquitto/libmosquitto_options.h 中MOSQ_OPT_PROTOCOL_VERSION的说明); - Broker 端对 v3.1 与 v3.1.1 的 CONNECT 均接受,且按
PROTOCOL_VERSION_v31/PROTOCOL_VERSION_v311分别处理(src/handle_connect.c); - 用户名密码的写入、置位与校验逻辑在 lib/send_connect.c 中对两个版本完全共用(
if(mosq->protocol == mosq_p_mqtt31 || mosq->protocol == mosq_p_mqtt311))。
可以说,v3.1 开创的用户名密码认证,是 MQTT 安全体系的地基:它在协议层面定义了"客户端如何自报身份",而 Mosquitto 则在此基础上叠加了密码文件校验、匿名策略、ACL 主题授权乃至 TLS/PSK 等多种安全机制,最终形成今天完整的接入控制链路。
七、小结
从 2010 年这篇博文到今天,MQTT v3.1 引入的用户名密码认证已完成从"规范选项"到"成熟实现"的演进:
| 层级 | v3.1 引入的能力 | Mosquitto 中的落地 |
|---|---|---|
| 协议层 | CONNECT 报文携带可选用户名/密码 | 客户端send__connect()构造,Brokerhandle_connect.c解析 |
| 协议标识 | 协议名MQIsdp、版本号 3 | include/mosquitto/mqtt_protocol.h 中PROTOCOL_NAME_v31 |
| 认证层 | 用户名密码校验 | password_file+mosquitto_passwd生成哈希文件 |
| 策略层 | 按用户名控制 Broker/主题访问 | allow_anonymous+acl_file按用户段授权 |
| 返回码 | 认证失败拒绝码 | CONNACK_REFUSED_BAD_USERNAME_PASSWORD(4) |
无论你使用的是 v3.1、v3.1.1 还是 v5 客户端,理解这条从 CONNECT 报文构造、密码文件校验到 ACL 授权的主链路,都能帮你更准确地排查连接被拒、权限不足等实际问题。
- 物联网
- 消息队列
- 后端
【免费下载链接】mosquitto
Eclipse Mosquitto - An open source MQTT broker
相关推荐
MQTT v3.1 认证演进:从 CONNECT 报文用户名/密码到 Mosquitto 的访问控制实战
MQTT v3.1 认证演进:从 CONNECT 报文用户名/密码到 Mosquitto 的访问控制实战 MQTT v3.1 是 MQTT 协议发展史上的关键里
物联网消息队列后端网络/通信Mosquitto 0.10 发布解读:MQTT v3.1 认证与 ACL 访问控制的实现与演进
Mosquitto 0.10 发布解读:MQTT v3.1 认证与 ACL 访问控制的实现与演进 导读 Eclipse Mosquitto 0.10 于 201
后端消息队列消息路由FastStream MQTT 安全配置实战指南:TLS 加密与 SASL 用户名密码认证
FastStream MQTT 安全配置实战指南:TLS 加密与 SASL 用户名密码认证 FastStream 的 MQTTBroker 与 Kafka、Ra
后端消息队列微服务
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考