MQTT 认证 -- EMQ X 认证功能踩坑 10000 字最全指南

概述

身份认证是大多数应用的重要组成部分,MQTT 协议支持用户名密码认证,启用身份认证能有效阻止非法客户端的连接。

本文介绍了 EMQ X 支持的认证方式以及对应插件的配置方法。

EMQ X MQTT Broker 简介

EMQ X Broker 是基于高并发的 Erlang/OTP 语言平台开发,支持百万级连接和分布式集群架构,发布订阅模式的开源 MQTT 消息服务器。

EMQ X Broker 在全球物联网市场广泛应用。无论是产品原型设计、物联网创业公司、还是大规模的商业部署,EMQ X Broker 都支持开源免费使用。

EMQ 官网Github 项目

认证方式

EMQ X 支持使用内置数据源(文件、内置数据库)、JWT、外部主流数据库和自定义 HTTP API 作为身份认证数据源。

连接数据源、进行认证逻辑通过插件实现的,每个插件对应一种认证方式,使用前需要启用相应的插件。

客户端连接时插件通过检查其 username/clientid 和 password 是否与指定数据源的信息一致来实现对客户端的身份认证。

EMQ X 支持的认证方式:

内置数据源

  • 用户名认证
  • Cliend ID 认证

使用配置文件与 EMQ X 内置数据库提供认证数据源,通过 REST API 进行管理,足够简单轻量。

外部数据库

  • MySQL 认证
  • PostgreSQL 认证
  • Redis 认证
  • MongoDB 认证

外部数据库可以存储大量数据,同时方便与外部设备管理系统集成。

其他

HTTP 认证
JWT 认证

JWT 认证可以批量签发认证信息,HTTP 认证能够实现复杂的认证鉴权逻辑。

更改插件配置后需要重启插件才能生效,部分认证鉴权插件包含 ACL 功能。

认证结果

任何一种认证方式最终都会返回一个结果:

  • 认证成功:经过比对客户端认证成功
  • 认证失败:经过比对客户端认证失败,数据源中密码与当前密码不一致
  • 忽略认证(ignore):当前认证方式中未查找到认证数据,无法显式判断结果是成功还是失败,交由认证链下一认证方式或匿名认证来判断

匿名认证

EMQ X 默认配置中启用了匿名认证,任何客户端都能接入 EMQ X。没有启用认证插件或认证插件没有显式允许/拒绝(ignore)连接请求时,EMQ X 将根据匿名认证启用情况决定是否允许客户端连接。

配置匿名认证开关:

# etc/emqx.conf

## Value: true | false
allow_anonymous = true

生产环境中请禁用匿名认证。

密码加盐规则与哈希方法

EMQ X 多数认证插件中可以启用哈希方法,数据源中仅保存密码密文,保证数据安全。

启用哈希方法时,用户可以为每个客户端都指定一个 salt(盐)并配置加盐规则,数据库中存储的密码是按照加盐规则与哈希方法处理后的密文。

以 MySQL 认证为例:

加盐规则与哈希方法配置:

# etc/plugins/emqx_auth_mysql.conf

## 不加盐,仅做哈希处理
auth.mysql.password_hash = sha256

## salt 前缀:使用 sha256 加密 salt + 密码 拼接的字符串
auth.mysql.password_hash = salt,sha256

## salt 后缀:使用 sha256 加密 密码 + salt 拼接的字符串
auth.mysql.password_hash = sha256,salt

## pbkdf2 with macfun iterations dklen
## macfun: md4, md5, ripemd160, sha, sha224, sha256, sha384, sha512
## auth.mysql.password_hash = pbkdf2,sha256,1000,20

如何生成认证信息

  1. 为每个客户端分用户名、Client ID、密码以及 salt(盐)等信息
  2. 使用与 MySQL 认证相同加盐规则与哈希方法处理客户端信息得到密文
  3. 将客户端信息写入数据库,客户端的密码应当为密文信息

EMQ X 身份认证流程

  1. 根据配置的认证 SQL 结合客户端传入的信息,查询出密码(密文)和 salt(盐)等认证数据,没有查询结果时,认证将终止并返回 ignore 结果
  2. 根据配置的加盐规则与哈希方法计算得到密文,没有启用哈希方法则跳过此步
  3. 将数据库中存储的密文与当前客户端计算的到的密文进行比对,比对成功则认证通过,否则认证失败

写入数据的加盐规则、哈希方法与对应插件的配置一致时认证才能正常进行。更改哈希方法会造成现有认证数据失效。

认证链

当同时启用多个认证方式时,EMQ X 将按照插件开启先后顺序进行链式认证:

  • 一旦认证成功,终止认证链并允许客户端接入
  • 一旦认证失败,终止认证链并禁止客户端接入
  • 直到最后一个认证方式仍未通过,根据匿名认证配置判定
    • 匿名认证开启时,允许客户端接入
    • 匿名认证关闭时,禁止客户端接入

emqx-认证

同时只启用一个认证插件可以提高客户端身份认证效率。


MySQL 认证

MySQL 认证使用外部 MySQL 数据库作为认证数据源,可以存储大量数据,同时方便与外部设备管理系统集成。

插件:

emqx_auth_mysql

emqx_auth_mysql 插件同时包含 ACL 功能,可通过注释禁用。

要启用 MySQL 认证,需要在 etc/plugins/emqx_auth_mysql.conf 中配置以下内容:

MySQL 连接信息

MySQL 基础连接信息,需要保证集群内所有节点均能访问。

# etc/plugins/emqx_auth_mysql.conf

## 服务器地址
auth.mysql.server = 127.0.0.1:3306

## 连接池大小
auth.mysql.pool = 8

auth.mysql.username = emqx

auth.mysql.password = public

auth.mysql.database = mqtt

auth.mysql.query_timeout = 5s

默认表结构

MySQL 认证默认配置下需要确保数据库中有下表:

CREATE TABLE `mqtt_user` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(100) DEFAULT NULL,
`password` varchar(100) DEFAULT NULL,
`salt` varchar(35) DEFAULT NULL,
`is_superuser` tinyint(1) DEFAULT 0,
`created` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `mqtt_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

默认配置下示例数据如下:

INSERT INTO `mqtt_user` ( `username`, `password`, `salt`)
VALUES
('emqx', 'efa1f375d76194fa51a3556a97e641e61685f914d446979da50a551a4333ffd7', NULL);

启用 MySQL 认证后,你可以通过用户名: emqx,密码:public 连接。

这是默认配置使用的表结构,熟悉该插件的使用后你可以使用任何满足条件的数据表进行认证。

加盐规则与哈希方法

MySQL 认证支持配置加盐规则与哈希方法。

# etc/plugins/emqx_auth_mysql.conf

auth.mysql.password_hash = sha256

认证 SQL(auth_query)

进行身份认证时,EMQ X 将使用当前客户端信息填充并执行用户配置的认证 SQL,查询出该客户端在数据库中的认证数据。

# etc/plugins/emqx_auth_mysql.conf

auth.mysql.auth_query = select password from mqtt_user where username = '%u' limit 1

你可以在认证 SQL 中使用以下占位符,执行时 EMQ X 将自动填充为客户端信息:

  • %u:用户名
  • %c:Client ID
  • %C:TLS 证书公用名(证书的域名或子域名),仅当 TLS 连接时有效
  • %d:TLS 证书 subject,仅当 TLS 连接时有效

你可以根据业务需要调整认证 SQL,如添加多个查询条件、使用数据库预处理函数,以实现更多业务相关的功能。但是任何情况下认证 SQL 需要满足以下条件:

  1. 查询结果中必须包含 password 字段,EMQ X 使用该字段与客户端密码比对
  2. 如果启用了加盐配置,查询结果中必须包含 salt 字段,EMQ X 使用该字段作为 salt(盐)值
  3. 查询结果只能有一条,多条结果时只取第一条作为有效数据

可以在 SQL 中使用 AS 语法为字段重命名指定 password,或者将 salt 值设为固定值。

特殊说明

MySQL 8.0 及以后版本使用了 caching_sha2_password 作为默认身份验证插件,受限于客户端驱动你必须将其更改为 mysql_native_password 插件:

ALTER USER 'your_username'@'your_host' IDENTIFIED WITH mysql_native_password BY 'your_password';

Redis 认证

Redis 认证使用外部 Redis 数据库作为认证数据源,可以存储大量数据,同时方便与外部设备管理系统集成。

插件:

emqx_auth_redis

emqx_auth_redis 插件同时包含 ACL 功能,可通过注释禁用。

要启用 Redis 认证,需要在 etc/plugins/emqx_auth_redis.conf 中配置以下内容:

Redis 连接信息

Redis 基础连接信息,需要保证集群内所有节点均能访问。

# etc/plugins/emqx_auth_redis.conf

## 服务器地址
auth.redis.server = 127.0.0.1:6379

## 连接池大小
auth.redis.pool = 8

auth.redis.database = 0

auth.redis.password =

默认数据结构

Redis 认证默认配置下使用哈希表存储认证数据,使用 mqtt_user: 作为 Redis 键前缀,数据结构如下:

redis> hgetall mqtt_user:emqx
password public
salt wivwiv

默认配置下示例数据如下:

HMSET mqtt_user:emqx password public salt wivwiv

启用 Redis 认证后,你可以通过用户名: emqx,密码:public 连接。

这是默认配置使用的数据结构,熟悉该插件的使用后你可以使用任何满足条件的数据结构进行认证。

加盐规则与哈希方法

Redis 认证支持配置 加盐规则与哈希方法,默认存储明文密码不做处理:

# etc/plugins/emqx_auth_redis.conf

auth.redis.password_hash = plain

认证查询命令(auth query cmd)

进行身份认证时,EMQ X 将使用当前客户端信息填充并执行用户配置的认证查询命令,查询出该客户端在 Redis 中的认证数据。

# etc/plugins/emqx_auth_redis.conf

auth.redis.auth_cmd = HMGET mqtt_user:%u password

你可以在命令中使用以下占位符,执行时 EMQ X 将自动填充为客户端信息:

  • %u:用户名
  • %c:Client ID
  • %C:TLS 证书公用名(证书的域名或子域名),仅当 TLS 连接时有效
  • %d:TLS 证书 subject,仅当 TLS 连接时有效

你可以根据业务需要调整认证查询命令,使用任意 Redis 支持的命令,但是任何情况下认证查询命令需要满足以下条件:

  1. 查询结果中第一个数据必须为 password,EMQ X 使用该字段与客户端密码比对
  2. 如果启用了加盐配置,查询结果中第二个数据必须是 salt 字段,EMQ X 使用该字段作为 salt(盐)值

MongoDB 认证

MongoDB 认证使用外部 MongoDB 数据库作为认证数据源,可以存储大量数据,同时方便与外部设备管理系统集成。

插件:

emqx_auth_mongo

emqx_auth_mongo 插件同时包含 ACL 功能,可通过注释禁用。

要启用 MongoDB 认证,需要在 etc/plugins/emqx_auth_mongo.conf 中配置以下内容:

MongoDB 连接信息

MongoDB 基础连接信息,需要保证集群内所有节点均能访问。

# etc/plugins/emqx_auth_mongo.conf

## MongoDB 架构类型
##
## Value: single | unknown | sharded | rs
auth.mongo.type = single

## rs 模式需要设置 rs name
## auth.mongo.rs_set_name =

## 服务器列表,集群模式下使用逗号分隔每个服务器
## Examples: 127.0.0.1:27017,127.0.0.2:27017...
auth.mongo.server = 127.0.0.1:27017

auth.mongo.pool = 8

auth.mongo.login =

auth.mongo.password =

## auth.mongo.auth_source = admin

auth.mongo.database = mqtt

auth.mongo.query_timeout = 5s

## SSL 选项
# auth.mongo.ssl = false

## auth.mongo.ssl_opts.keyfile =

## auth.mongo.ssl_opts.certfile =

## auth.mongo.ssl_opts.cacertfile =

## MongoDB write mode.
##
## Value: unsafe | safe
## auth.mongo.w_mode =

## Mongo read mode.
##
## Value: master | slave_ok
## auth.mongo.r_mode =

## MongoDB 拓扑配置,一般情况下用不到,详见 MongoDB 官网文档
auth.mongo.topology.pool_size = 1
auth.mongo.topology.max_overflow = 0
## auth.mongo.topology.overflow_ttl = 1000
## auth.mongo.topology.overflow_check_period = 1000
## auth.mongo.topology.local_threshold_ms = 1000
## auth.mongo.topology.connect_timeout_ms = 20000
## auth.mongo.topology.socket_timeout_ms = 100
## auth.mongo.topology.server_selection_timeout_ms = 30000
## auth.mongo.topology.wait_queue_timeout_ms = 1000
## auth.mongo.topology.heartbeat_frequency_ms = 10000
## auth.mongo.topology.min_heartbeat_frequency_ms = 1000

默认表结构

MongoDB 认证默认配置下需要确保数据库中有如下集合:

{
username: "user",
password: "password hash",
salt: "password salt",
is_superuser: false,
created: "2020-02-20 12:12:14"
}

默认配置下示例数据如下:

use mqtt

db.mqtt_user.insert({
"username": "emqx",
"password": "efa1f375d76194fa51a3556a97e641e61685f914d446979da50a551a4333ffd7",
"salt": ""
})

启用 MongoDB 认证后,你可以通过用户名: emqx,密码:public 连接。

这是默认配置使用的集合结构,熟悉该插件的使用后你可以使用任何满足条件的集合进行认证。

加盐规则与哈希方法

MongoDB 认证支持配置加盐规则与哈希方法:

# etc/plugins/emqx_auth_mongo.conf

auth.mongo.password_hash = sha256

认证查询(auth_selector)

进行身份认证时,EMQ X 将使用当前客户端信息填充并执行用户配置的认证 SQL,查询出该客户端在数据库中的认证数据。

MongoDB 支持配置集合名称、密码字段、selector 命令

# etc/plugins/emqx_auth_mongo.conf

auth.mongo.auth_query.collection = mqtt_user

## 如果启用了加盐处理,此处需配置为 password,salt
## Value: password | password,salt
auth.mongo.auth_query.password_field = password

auth.mongo.auth_query.selector = username=%u

你可以在认证查询(selector)中使用以下占位符,执行时 EMQ X 将自动填充为客户端信息:

  • %u:用户名
  • %c:Client ID
  • %C:TLS 证书公用名(证书的域名或子域名),仅当 TLS 连接时有效
  • %d:TLS 证书 subject,仅当 TLS 连接时有效

你可以根据业务需要调整认证查询,如添加多个查询条件、使用数据库预处理函数,以实现更多业务相关的功能。但是任何情况下认证查询需要满足以下条件:

  1. 查询结果中必须包含 password 字段,EMQ X 使用该字段与客户端密码比对
  2. 如果启用了加盐配置,查询结果中必须包含 salt 字段,EMQ X 使用该字段作为 salt(盐)值
  3. MongoDB 使用 findOne 查询命令,确保你期望的查询结果能够出现在第一条数据中

JWT 认证

JWT 认证基于 Token 的鉴权机制,不依赖服务端保留客户端的认证信息或者会话信息,在持有密钥的情况下可以批量签发认证信息,是最简便的认证方式。

插件:

emqx_auth_jwt

认证原理

客户端使用 Token 作为用户名或密码(取决于插件配置),发起连接时 EMQ X 使用配置中的密钥、证书进行解密,如果能成功解密则认证成功,否则认证失败。

默认配置下启用 JWT 认证后,你可以通过任意用户名,以下密码进行连接:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImF1dGhvciI6IndpdndpdiIsInNpdGUiOiJodHRwczovL3dpdndpdi5jb20ifSwiZXhwIjoxNTgyMjU1MzYwNjQyMDAwMCwiaWF0IjoxNTgyMjU1MzYwfQ.FdyAx2fYahm6h3g47m88ttyINzptzKy_speimyUcma4

配置项

要启用 JWT 认证,需要在 etc/plugins/emqx_auth_jwt.conf 中配置以下内容:

# etc/plugins/emqx_auth_jwt.conf

## 密钥
auth.jwt.secret = emqxsecret

## 客户端携带 Token 的方式
## Value: username | password
auth.jwt.from = password


## 高级选项
## 公钥文件,证书作为签发密钥时使用
auth.jwt.pubkey = etc/certs/jwt_public_key.pem

## Value: on | off
auth.jwt.verify_claims = off

## auth.jwt.verify_claims.$name = expected

## Variables:
## - %u: username
## - %c: clientid
# auth.jwt.verify_claims.username = %u

JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限,使用 JWT 时建议启用 TLS 加密传输。
JWT 使用过程中无法在过期前废止某个 Token,请妥善设置有效时长并保管好密钥等加密信息。


HTTP 认证

HTTP 认证使用外部自建 HTTP 应用认证数据源,根据 HTTP API 返回的数据判定认证结果,能够实现复杂的认证鉴权逻辑。

插件:

emqx_auth_http

emqx_auth_http 插件同时包含 ACL 功能,可通过注释禁用。

认证原理

EMQ X 在设备连接事件中使用当前客户端相关信息作为参数,向用户自定义的认证服务发起请求查询权限,通过返回的 HTTP 响应状态码 (HTTP statusCode) 来处理认证请求。

  • 认证失败:API 返回 4xx 状态码
  • 认证成功:API 返回 200 状态码
  • 忽略认证:API 返回 200 状态码且消息体 ignore

HTTP 请求信息

HTTP API 基础请求信息,配置证书、请求头与重试规则。

# etc/plugins/emqx_auth_http.conf

## 启用 HTTPS 所需证书信息
## auth.http.ssl.cacertfile = etc/certs/ca.pem

## auth.http.ssl.certfile = etc/certs/client-cert.pem

## auth.http.ssl.keyfile = etc/certs/client-key.pem

## 请求头设置
## auth.http.header.Accept = */*

## 重试设置
auth.http.request.retry_times = 3

auth.http.request.retry_interval = 1s

auth.http.request.retry_backoff = 2.0

加盐规则与哈希方法

HTTP 在请求中传递明文密码,加盐规则与哈希方法取决于 HTTP 应用。

认证请求

进行身份认证时,EMQ X 将使用当前客户端信息填充并执行用户配置的认证 SQL,查询出该客户端在数据库中的认证数据。

# etc/plugins/emqx_auth_http.conf

## 请求地址
auth.http.auth_req = http://127.0.0.1:8991/mqtt/auth

## HTTP 请求方法
## Value: post | get | put
auth.http.auth_req.method = post

## 请求参数
auth.http.auth_req.params = clientid=%c,username=%u,password=%P

HTTP 请求方法为 GET 时,请求参数将以 URL 查询字符串的形式传递;POST、PUT 请求则将请求参数以普通表单形式提交(content-type 为 x-www-form-urlencoded)。

你可以在认证请求中使用以下占位符,请求时 EMQ X 将自动填充为客户端信息:

  • %u:用户名
  • %c:Client ID
  • %a:客户端 IP 地址
  • %r:客户端接入协议
  • %P:明文密码
  • %p:客户端端口
  • %C:TLS 证书公用名(证书的域名或子域名),仅当 TLS 连接时有效
  • %d:TLS 证书 subject,仅当 TLS 连接时有效

推荐使用 POST 与 PUT 方法,使用 GET 方法时明文密码可能会随 URL 被记录到传输过程中的服务器日志中。