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 增强认证的流程:

MQTT 5.0 增强认证流程 MQTT 客户端 MQTT Broker CONNECT (认证方法=SCRAM) AUTH (挑战数据) AUTH (响应数据) CONNACK (成功) 说明 客户端和 Broker 可以通过多次 AUTH 报文交换 认证信息,直到双方达成一致或超时失败

看到没?这个流程比传统的用户名/密码复杂得多,但安全性也高得多。SCRAM 认证可以防止密码被明文传输,即使网络被监听,攻击者也拿不到原始密码。

最后,我想强调一点:无论你用哪种认证方式,传输层的安全永远是第一位的。MQTT 5.0 的认证数据属性再强大,如果底层没有 TLS 加密,认证数据本身还是可能被截获。所以,我的建议是:TLS + 增强认证,两者结合,才能构建真正安全的 MQTT 系统。