MQTT 身份认证基础
好,咱们正式开始聊身份认证。说实话,MQTT 协议在设计之初,对安全这块考虑得并不算多。它最早是为工业传感器网络设计的,那时候大家觉得设备都在内网,没必要搞太复杂。但放到今天,设备直接暴露在公网上,你再不搞认证,那基本等于把家门敞开。
这一章,我就带你看看 MQTT 协议里到底提供了哪些身份认证的“抓手”。说白了,就是 Broker 怎么知道你是谁,以及你有没有权限连上来。
CONNECT 报文中的用户名/密码
这是最基础、也是最常见的认证方式。任何 MQTT 客户端想要连上 Broker,第一步就是发一个 CONNECT 报文。这个报文里,有两个字段是专门用来做身份认证的:用户名(Username) 和 密码(Password)。
你想想看,这就像你去公司上班,进门要刷工牌。工牌上的名字就是用户名,密码就是你每天输入的 PIN 码。Broker 收到这个报文后,会去查自己的用户列表,看看用户名和密码是否匹配。
核心要点:用户名和密码在 CONNECT 报文中是明文传输的。对,你没听错,是明文。除非你用了 TLS 加密整个连接,否则这些信息在网络上是裸奔的。
我在项目里就遇到过这种情况。有一次帮客户做智能水表的远程监控,他们觉得“内网安全”,就没开 TLS。结果我用 Wireshark 一抓包,所有设备的用户名密码看得清清楚楚。嗯,从那以后,我养成了一个习惯:只要涉及生产环境,TLS 必须开,没有例外。
来看一下 CONNECT 报文的典型结构:
CONNECT 报文固定头:
- 报文类型: 0x10
- 剩余长度: 可变
可变头:
- 协议名: "MQTT"
- 协议级别: 4 (MQTT 3.1.1) 或 5 (MQTT 5.0)
- 连接标志: 包含用户名标志、密码标志等
- 保持连接: 心跳间隔
有效载荷(Payload):
- 客户端标识符 (ClientID)
- 用户名 (如果用户名标志为 1)
- 密码 (如果密码标志为 1)
注意看连接标志里的“用户名标志”和“密码标志”。这两个标志位决定了 Broker 是否要检查用户名和密码。如果标志位为 0,那 Payload 里就没有这两个字段,Broker 就会允许匿名连接。
警告:生产环境中,务必关闭匿名连接。我见过太多因为忘记设置用户名标志位,导致任何人都能连上 Broker 的案例。这不是技术问题,是安全意识问题。
ClientID 的作用与风险
ClientID,也就是客户端标识符。每个 MQTT 客户端在连接时,都必须提供一个唯一的 ClientID。Broker 用这个 ID 来跟踪客户端的会话状态、订阅关系,以及离线消息。
ClientID 的作用其实有三个:
- 会话标识:Broker 根据 ClientID 来维护会话。如果客户端断线重连,只要 ClientID 不变,Broker 就能恢复之前的订阅和未送达的消息。
- 身份关联:很多系统会把 ClientID 和具体的设备绑定。比如,一个温度传感器的 ClientID 可能是
temp_sensor_001。 - 安全边界:在授权策略中,经常用 ClientID 来做细粒度的权限控制。比如,只允许
temp_sensor_001发布到sensors/temperature/001这个主题。
但这里有个坑。ClientID 本身并不是一个安全凭证。它只是一个标识符,任何人都可以伪造。我曾经在测试一个智能家居平台时发现,攻击者只要知道某个设备的 ClientID,就能冒充它连接 Broker,前提是 Broker 没有做额外的认证。
我的建议:不要把 ClientID 当成密码来用。它应该和用户名/密码或者证书配合使用。另外,ClientID 的命名最好有一定的规则,比如包含设备类型、批次号、随机数,这样即使泄露了,也不容易被批量利用。
来看一个 ClientID 的典型命名示例:
格式:{设备类型}_{批次号}_{随机字符串}
示例:gateway_batch03_a8f3c2d1
sensor_temp_2024_9b7e1f5a
actuator_valve_001_4c6d8e2f
这种命名方式的好处是,即使攻击者拿到了一个 ClientID,他也很难推断出其他设备的 ID 规律。嗯,这算是一种“通过模糊化提升安全性”的手段,虽然不能完全依赖它,但至少能增加攻击成本。
MQTT 5.0 新增的认证数据属性
MQTT 5.0 在身份认证这块做了很大的改进。它引入了一个新的属性——认证数据(Authentication Data)。这个属性让 MQTT 不再局限于简单的用户名/密码认证,而是可以支持更灵活的认证机制,比如 SCRAM、Kerberos,甚至是自定义的 Token 认证。
说白了,MQTT 5.0 把认证这件事“插件化”了。你可以在 CONNECT 报文里带上一个 认证方法(Authentication Method) 字段,告诉 Broker 你要用哪种认证方式。然后,通过 认证数据 字段来传递具体的认证信息。
我个人觉得,这是 MQTT 5.0 最实用的改进之一。为什么?因为在实际项目中,用户名/密码的局限性太大了。比如,你对接的是企业的 LDAP 系统,或者你想用 JWT Token 来做无状态认证,MQTT 3.1.1 就很难办。但 MQTT 5.0 的认证数据属性,让这一切变得可能。
来看一下 MQTT 5.0 CONNECT 报文中的认证相关字段:
| 字段 | MQTT 3.1.1 | MQTT 5.0 |
|---|---|---|
| 用户名 | 支持 | 支持 |
| 密码 | 支持 | 支持 |
| 认证方法 | 不支持 | 新增 |
| 认证数据 | 不支持 | 新增 |
| 增强认证 | 不支持 | 支持(通过 AUTH 报文) |
注意最后一行——增强认证。MQTT 5.0 不仅支持在 CONNECT 报文中传递认证数据,还支持通过单独的 AUTH 报文进行多轮交互认证。这有点像 HTTP 的 Digest 认证,客户端和服务器可以来回交换信息,直到认证完成。
我记得在做一个车联网项目时,客户要求使用双向证书认证,但同时又想保留用户名/密码作为备用方案。MQTT 5.0 的认证数据属性完美解决了这个问题:我们可以先尝试证书认证,如果失败,再回退到用户名/密码。这种灵活性,在 MQTT 3.1.1 里几乎不可能实现。
关键区别:MQTT 3.1.1 的认证是“一次性”的,CONNECT 报文发完,认证就结束了。MQTT 5.0 的认证可以是“多轮”的,通过 AUTH 报文,客户端和 Broker 可以反复协商,直到双方达成一致。
下面我用一张图来展示 MQTT 5.0 增强认证的流程:
看到没?这个流程比传统的用户名/密码复杂得多,但安全性也高得多。SCRAM 认证可以防止密码被明文传输,即使网络被监听,攻击者也拿不到原始密码。
最后,我想强调一点:无论你用哪种认证方式,传输层的安全永远是第一位的。MQTT 5.0 的认证数据属性再强大,如果底层没有 TLS 加密,认证数据本身还是可能被截获。所以,我的建议是:TLS + 增强认证,两者结合,才能构建真正安全的 MQTT 系统。