认证(认证鉴权)

认证(认证鉴权)指的是当一个客户端连接到 MQTT 服务器的时候,通过服务器端的配置来控制客户端连接服务器的权限。EMQ X 的认证支持包括多个层面,分别有 MQTT 传输层,应用层和 EMQ X 本身以插件的方式来支持各种增强的认证方式。

  • 在传输层上,TLS 可以保证使用客户端证书的客户端到服务器的身份验证,并确保服务器向客户端验证服务器证书
  • 在应用层上,MQTT 协议本身在 CONNECT 报文中指定用户名和密码。客户端可以在连接到 MQTT 服务器时发送用户名和密码进行认证,有效阻止非法客户端的连接
  • EMQ X 层面上,以插件形式支持配置文件、HTTP API、JWT、LDAP 及各类数据库如 MongoDB、MySQL、PostgreSQL、Redis 等多种认证

认证与认证链

EMQ X 默认开启匿名认证,即允许任意客户端登录,具体配置在 etc/emqx.conf 中:

  1. ## Allow Anonymous authentication
  2. mqtt.allow_anonymous = true

EMQ X 认证相关插件名称以 emqx_auth 开头。当启用认证插件之前,请在配置文件 etc/emqx.conf 中把允许匿名认证的方式给去掉:mqtt.allow_anonymous = false。当共同启用多个认证插件时,EMQ X 将按照插件开启先后顺序进行链式认证,一旦认证成功就终止认证链并允许客户端接入,最后一个认证源仍未通过时将终止客户端连接,认证链的认证过程示意图如下所示。

auth chain

用户名密码认证

用户名密码认证使用配置文件存储用户名与密码,通过 username 与 password 进行连接认证。

打开并配置 etc/plugins/emqx_auth_username.conf 文件,按照如下所示创建认证信息:

  1. # 第一组认证信息
  2. auth.user.1.username = username
  3. auth.user.1.password = passwd
  4. # 第二组认证信息
  5. auth.user.2.username = default_user
  6. auth.user.2.password = passwd2

在 EMQ X Dashboard 或控制台启用插件:

./bin/emqx_ctl plugins load emqx_auth_username

然后重启 emqx 服务。如果配置成功正确,使用正确的用户名和密码可以连接成功,而指定的错误的用户名和密码,通过 mosquitto 提供的命令行会报以下的错误。读者在修改了配置文件后,需要重新启动 emqx 服务才可以生效

  1. # mosquitto_sub -h $your_host -u username -P passwd1 -t /devices/001/temp
  2. Connection Refused: bad user name or password.

在错误日志文件 /var/log/emqx/error.log 中会有类似于以下的错误信息。

  1. [error] <0.1981.0>@emqx_protocol:process:241 Client(mosqsub/10166-master@10.211.55.6:40177): Username 'username' login failed for "No auth module to check!"

ClientID 认证

ClientID 认证使用配置文件存储客户端 ID 与密码,连接时通过 clientid 与 password 进行认证。

配置 etc/plugins/emqx_auth_clientid.conf 文件,按照如下所示创建认证信息:

  1. # 第一组认证信息
  2. auth.client.1.clientid = id
  3. auth.client.1.password = passwd
  4. # 第二组认证信息
  5. auth.client.2.clientid = dev:devid
  6. auth.client.2.password = passwd2

在 EMQ X Dashboard 或控制台启用插件:

./bin/emqx_ctl plugins load emqx_auth_clientid

重启 emqx 服务后,可通过 MQTT 客户端通过在上述配置文件中配置的客户端 id 和密码连接至 EMQ,如果指定了错误的客户端 ID 和密码,使用 mosquitto_sub 的时候会出现如下错误。。

  1. # mosquitto_sub -h $your_host -u id -i id1 -P passwd -t /devices/001/temp
  2. Connection Refused: bad user name or password.

HTTP 认证

HTTP 认证调用自定义的 HTTP API 实现认证鉴权。

实现原理

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

  • 认证成功,API 返回 200 状态码

  • 认证失败,API 返回 4xx 状态码

使用方式

打开 etc/plugins/emqx_auth_http.conf 文件,配置相关规则:

  1. ## 配置一个认证请求 URL,地址的路径部分“/auth/AuthServlet”,用户可以自己随便定义
  2. auth.http.auth_req = http://$SERVER:$port/auth/AuthServlet
  3. ## HTTP 请求方法
  4. auth.http.auth_req.method = post
  5. ## 使用占位符传递请求参数
  6. auth.http.auth_req.params = clientid=%c,username=%u,password=%P

启用插件并且重启 EMQ X 服务器之后,所有的连接将通过 http://$SERVER:8080/auth/AuthServlet 进行认证,该服务获取到参数并执行相关验证逻辑后返回相应的 HTTP 响应状态码。但是具体返回内容视你自己需求而定,EMQ X 不作要求。

以下为一段 Java Servlet 代码示例:

  • 当连接的 clientId,username,password 中任意一个为空的时候,返回状态码 SC_BAD_REQUEST (400) ,表示参数有问题
  • 当 clientId 为 id1,username 为 user1,password 为 passwd 的时候,返回状态码 OK(200) ,表示认证通过;否则返回 SC_UNAUTHORIZED(401),表示认证失败
  1. package io.emqx;
  2. import java.io.IOException;
  3. import java.text.MessageFormat;
  4. import javax.servlet.ServletException;
  5. import javax.servlet.annotation.WebServlet;
  6. import javax.servlet.http.HttpServlet;
  7. import javax.servlet.http.HttpServletRequest;
  8. import javax.servlet.http.HttpServletResponse;
  9. @WebServlet("/AuthServlet")
  10. public class AuthServlet extends HttpServlet {
  11. private static final long serialVersionUID = 1L;
  12. public AuthServlet() {
  13. super();
  14. }
  15. protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  16. String clientId = request.getParameter("clientid");
  17. String username = request.getParameter("username");
  18. String password = request.getParameter("password");
  19. System.out.println(MessageFormat.format("clientid: {0}, username: {1}, password:{2}", clientId, username, password));
  20. if(clientId == null || "".equals(clientId.trim()) || username == null || "".equals(username) || password == null || "".equals(password.trim())) {
  21. response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
  22. response.getWriter().println("Invalid request contents.");
  23. return;
  24. }
  25. if("id1".equals(clientId) && "user1".equals(username) && "passwd".equals(password)) {
  26. response.setStatus(HttpServletResponse.SC_OK);
  27. response.getWriter().println("OK");
  28. } else {
  29. response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
  30. response.getWriter().println("Invalid user credentials.");
  31. return;
  32. }
  33. }
  34. protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  35. doGet(request, response);
  36. }
  37. }

命令行中输入以下内容,连接成功。

  1. # mosquitto_sub -h 10.211.55.10 -u user1 -i id1 -P passwd -t /devices/001/temp

在 web 服务器中输出以下内容,表明验证信息成功传入。

  1. clientid: id1, username: user1, password:passwd

如果指定了错误的信息,认证将失败。如下所示,指定错误的用户名 user

  1. # mosquitto_sub -h 10.211.55.10 -u user -i id1 -P passwd -t /devices/001/temp
  2. Connection Refused: bad user name or password.

JWT 认证

TODO

LDAP 认证

TODO

MySQL/PostgreSQL 认证

emqx_auth_mysql / emqx_auth_pgsql 分别为基于 MySQL、PostgreSQL 数据库的认证 / 访问控制插件。EMQ X 将根据插件配置,使用当前客户端信息生成预定 SQL 语句,查询数据库进行认证操作。

Auth 配置

MySQL 的安装过程请读者参考网上相关文章,此处不再赘述。

创建数据库

读者可以使用任何自己喜欢的 mysql 客户端,创建好相应的数据库。这里用的是 MySQL 自带的命令行客户端,打开 MySQL 的控制台,如下所示,创建一个名为 emqx 的认证数据库,并切换到 emqx 数据库。

  1. mysql> create database emqx;
  2. Query OK, 1 row affected (0.00 sec)
  3. mysql> use emqx;
  4. Database changed

创建表

建议的表结构如下,其中,

  • username 为客户端连接的时候指定的用户名
  • password_hash 为使用 salt 加密后的密文
  • salt 为加密串
  • is_superuser 是否为超级用户,用于控制 ACL,缺省为0;设置成1的时候为超级用户,跳过 ACL 检查。具体请参考 ACL(Access Control List)访问控制。注:读者在生成的表格中,字段可以不用完全跟下面的一致,用户可以通过配置 emqx_auth_mysql.conf 文件中的 authquery 的 SQL 语句来适配)。
  1. CREATE TABLE `mqtt_user` (
  2. `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  3. `username` varchar(100) DEFAULT NULL,
  4. `password_hash` varchar(255) DEFAULT NULL,
  5. `salt` varchar(40) DEFAULT NULL,
  6. `is_superuser` tinyint(1) DEFAULT 0,
  7. `created` datetime DEFAULT NULL,
  8. PRIMARY KEY (`id`),
  9. UNIQUE KEY `mqtt_username` (`username`)
  10. ) ENGINE=MyISAM DEFAULT CHARSET=utf8;

创建成功后,查看一下表结构如下,

  1. mysql> desc mqtt_user;
  2. +---------------+------------------+------+-----+---------+----------------+
  3. | Field | Type | Null | Key | Default | Extra |
  4. +---------------+------------------+------+-----+---------+----------------+
  5. | id | int(11) unsigned | NO | PRI | NULL | auto_increment |
  6. | username | varchar(100) | YES | UNI | NULL | |
  7. | password_hash | varchar(255) | YES | | NULL | |
  8. | salt | varchar(40) | YES | | NULL | |
  9. | is_superuser | tinyint(1) | YES | | 0 | |
  10. | created | datetime | YES | | NULL | |
  11. +---------------+------------------+------+-----+---------+----------------+
  12. 6 rows in set (0.01 sec)

准备认证数据

比较查询结果中的 password 字段的值是否与当前客户端的密码进行加盐加密后的值是否相等,验证流程如下:

  • 查询结果集中必须有 password 字段;
  • 在数据库中可以为每个客户端都指定一个 salt,EMQ X 根据客户端传入的密码和通过 SQL 返回的 salt 信息生成密文;
  • 结果集为空或两个字段不等,认证失败。插入示例数据,示例数据中密码为 test_password,加密 salt 为 secret,在 EMQ X 的配置文件的 auth.mysql.password_hash 中,salt 只是一个标识符,不代表使用该字符进行加盐处理

  • 如果采用auth.mysql.password_hash = md5,salt ,那么 EMQ X 使用 MD5 算法对 test_passwordsecret 字符串加密;

  • 如果采用auth.mysql.password_hash = salt,md5 ,那么 EMQ X 使用 MD5 算法对 secrettest_password 字符串加密;本文采用第一种配置方式,将得到的 MD5 密文插入表 mqtt_user。读者可以通过在线的 MD5 工具或者自己写程序对密码进行编码。
  1. MD5("test_passwordsecret") -> a904b2d1d2b2f73de384955022964595
  1. mysql> INSERT INTO mqtt_user(username,password_hash,salt) VALUES('test_username', 'a904b2d1d2b2f73de384955022964595', 'secret');
  2. Query OK, 1 row affected (0.00 sec)
  3. mysql> select * from mqtt_user;
  4. +----+----------------+----------------------------------+--------+--------------+---------+
  5. | id | username | password_hash | salt | is_superuser | created |
  6. +----+----------------+----------------------------------+--------+--------------+---------+
  7. | 3 | test_username1 | a904b2d1d2b2f73de384955022964595 | secret | 0 | NULL |
  8. +----+----------------+----------------------------------+--------+--------------+---------+
  9. 1 row in set (0.00 sec)

修改 EMQ X 配置文件

由于表中没有 password 字段,查询 SQL 应该使用 AS 语法来转换处理:

  1. SELECT password_hash AS password, ...

修改后的主要配置如下所示,其它相关配置,请参考配置文件中相应的描述进行修改。

  1. ## 修改为实际 mysql 所在的服务器地址
  2. auth.mysql.server = $mysql_host:3306
  3. ## 修改为上面创建成功的 emqx 数据库
  4. auth.mysql.database = emqx
  5. auth.mysql.auth_query = SELECT password_hash AS password, salt FROM mqtt_user WHERE username = '%u'
  6. ## 加密算法 plain | md5 | sha | sha256 | bcrypt
  7. ## 加盐加密算法
  8. auth.mysql.password_hash = md5,salt
  9. ## 不加盐加密算法,直接写算法名称即可
  10. # auth.mysql.password_hash = md5

PostgreSQL 的用法与 MySQL 类似,读者可以参考本章的配置,此处不再赘述。

Redis 认证

Auth 配置

客户端上线后,Redis 认证插件连接至 Redis ,通过查询和比对 Redis 中预先存储的认证信息来判断该客户端是否有权限连接该服务器。

Redis 安装

读者可以参考数据持久化部分中的安装与验证 redis 服务器章节来完成 Redis 的安装,此处不再赘述。

准备数据

需要先将认证数据存入 Redis 数据库中,推荐使用 : 作为 Redis key 的分隔符,为避免 key 与其他业务重复,建议可以加入一个业务标识符前缀,key 的格式如下。

  1. # 业务标识符前缀:username 或 clientid
  2. prefix:[username|clientid]

mqtt_user:userid_001

通过 Redis 提供的命令行工具 redis-cli 来将认证数据导入到 Redis Hash 数据结构中,读者可以参考 hmsethget 获取更加详细的介绍。

  1. ## 将 key 为 mqtt_user:userid_001;设置密码字段为 public,设置is_superuser字段为 false
  2. 127.0.0.1:6379[2]> HMSET mqtt_user:userid_001 password "public" is_superuser false
  3. OK
  4. ## 列出所有的 key,刚存入的被列出
  5. 127.0.0.1:6379[2]> keys *
  6. 1) "mqtt_user:userid_001"
  7. ## 展示客户端的 key 为 mqtt_user:userid_001 的 password 字段值
  8. 127.0.0.1:6379> hget mqtt_user:userid_001 password
  9. "public"
  10. ## 展示客户端的 key 为 mqtt_user:userid_001 的 is_superuser 字段值
  11. 127.0.0.1:6379> hget mqtt_user:userid_001 is_superuser
  12. "false"

修改配置文件

emqx_auth_redis 插件将根据插件配置,根据传入的客户端信息生成相应的 Redis 命令,查询结果进行比较。

打开 etc/plugins/emqx_auth_redis.conf,配置以下信息:

  1. ## 认证时执行的 Redis 命令
  2. auth.redis.auth_cmd = HMGET mqtt_user:%u password
  3. ## Password hash
  4. auth.redis.password_hash = plain

配置完毕后执行 emqx_ctl plugins load emqx_auth_redis 并重启 emqx 服务。在客户端使用 mosquitto_sub 命令来连接。

  1. ## 使用错误的用户名和密码
  2. # mosquitto_sub -h 10.211.55.10 -u userid_001 -P password -t /devices/001/temp
  3. Connection Refused: bad user name or password.
  4. ## 使用正确的用户名和密码,加入 -d 参数,打印交互的 MQTT 报文
  5. # mosquitto_sub -h 10.211.55.10 -u userid_001 -P public -t /devices/001/temp -d
  6. Client mosqsub/18771-master sending CONNECT
  7. Client mosqsub/18771-master received CONNACK
  8. Client mosqsub/18771-master sending SUBSCRIBE (Mid: 1, Topic: /devices/001/temp, QoS: 0)
  9. Client mosqsub/18771-master received SUBACK
  10. Subscribed (mid: 1): 0

密码加密、加盐

上文描述的是在 Redis 中采用明文的方式保存密码,EMQ X 还支持用加密算法对密码进行加密和加盐处理。

  • 修改配置文件:打开配置文件 emqx_auth_redis.conf
    • 更改配置 auth.redis.password_hash = salt,sha256 ,采用 sha256 加密算法,加入的 salt 在密码之前;如果该配置是 sha256,salt 则表示加入的 salt 在密码之后;注意:salt 只是一个标识符,不代表使用该字符进行加盐处理
    • 更改读取命令 auth.redis.auth_cmd ,需要取出 salt;
    • 更改完成后重启 EMQ X 服务。
  1. ## 更改认证时执行的 Redis 命令,取出 salt
  2. auth.redis.auth_cmd = HMGET mqtt_user:%u password salt
  3. ## sha256 with salt prefix
  4. auth.redis.password_hash = salt,sha256
  • 在 Redis 中存入数据,根据上一步的配置,假设该客户端设置的 salt 为 mysalt,那么加盐后的密码原文为 mysaltpublic ,读者可以通过在线的 sha256工具将密码转换为密文,并存入 Redis。
  1. sha256("mysaltpublic") -> 129735f3af16d9a3a6784752d034542642ec96728b6f1dd47ec2b6fe46137130

打开 Redis 命令行工具。

  1. ## 先删除之前保存的认证数据
  2. 127.0.0.1:6379> del mqtt_user:userid_001
  3. (integer) 1
  4. ## 保存认证数据
  5. 127.0.0.1:6379> HMSET mqtt_user:userid_001 password "129735f3af16d9a3a6784752d034542642ec96728b6f1dd47ec2b6fe46137130" is_superuser false salt "mysalt"
  6. OK
  7. ## 取出相关的密码和盐
  8. 127.0.0.1:6379> HMGET mqtt_user:userid_001 password salt
  9. 1) "129735f3af16d9a3a6784752d034542642ec96728b6f1dd47ec2b6fe46137130"
  10. 2) "mysalt"
  • 在客户端使用 mosquitto_sub 命令来连接。
  1. # mosquitto_sub -h 10.211.55.10 -u userid_001 -P public -t /devices/001/temp -d
  2. Client mosqsub/9936-master sending CONNECT
  3. Client mosqsub/9936-master received CONNACK
  4. Client mosqsub/9936-master sending SUBSCRIBE (Mid: 1, Topic: /devices/001/temp, QoS: 0)
  5. Client mosqsub/9936-master received SUBACK
  6. Subscribed (mid: 1): 0

MongoDB 认证

Auth 配置

客户端上线后,MongoDB 认证插件连接至 MongoDB ,通过查询和比对 MongoDB 中预先存储的认证信息来判断该客户端是否有权限连接该服务器。

MongoDB 安装

读者请按照 MongoDB 安装文档安装好数据库,然后使用客户端 mongo 连接到数据库。

  1. # mongo
  2. MongoDB shell version v4.0.4
  3. connecting to: mongodb://127.0.0.1:27017
  4. Implicit session: session { "id" : UUID("373cf3b1-d72d-4292-95cc-a76cfd607fa7") }
  5. MongoDB server version: 4.0.4
  6. Welcome to the MongoDB shell.
  7. ......

准备数据

emqx_auth_mongo 插件根据配置的存储客户端信息的集合(collection)、password 字段名(password_field)、过滤查询的 selector 进行认证操作:

MongoDB mqtt 数据库中有如下信息:

  1. ## 插入数据
  2. > use mqtt
  3. switched to db mqtt
  4. > db.mqtt_user.insert({ username: 'userid_001', password: 'public', is_superuser: false })
  5. WriteResult({ "nInserted" : 1 })
  6. ## 查看数据
  7. > db.mqtt_user.find({})
  8. { "_id" : ObjectId("5be795f7744a3bac99a6fd02"), "username" : "userid_001", "password" : "public", "is_superuser" : false }

修改配置文件

打开 etc/plugins/emqx_auth_mongo.conf,配置以下信息:

  1. ## Mongo 认证数据库名称
  2. auth.mongo.database = mqtt
  3. ## 认证信息所在集合
  4. auth.mongo.auth_query.collection = mqtt_user
  5. ## 密码字段
  6. auth.mongo.auth_query.password_field = password
  7. ## 使用明文密码存储
  8. auth.mongo.auth_query.password_hash = plain
  9. ## 查询指令
  10. auth.mongo.auth_query.selector = username=%u

配置完毕后执行 emqx_ctl plugins load emqx_auth_mongo 并重启 emqx 服务。usernameuserid_001 的客户端连接时,EMQ X 将执行下列查询:

  1. > db.mqtt_user.findOne({ username: 'userid_001' })
  2. {
  3. "_id" : ObjectId("5be795f7744a3bac99a6fd02"),
  4. "username" : "userid_001",
  5. "password" : "public",
  6. "is_superuser" : false
  7. }

当查询结果中的 password(password_field 字段)与当前客户端 password 相等时,认证成功。在客户端使用 mosquitto_sub 命令来连接。

  1. ## 使用错误的用户名和密码
  2. # mosquitto_sub -h 10.211.55.10 -u userid_001 -P password -t /devices/001/temp
  3. Connection Refused: bad user name or password.
  4. ## 使用正确的用户名和密码,加入 -d 参数,打印交互的 MQTT 报文
  5. # mosquitto_sub -h 10.211.55.10 -u userid_001 -P public -t /devices/001/temp -d
  6. Client mosqsub/18771-master sending CONNECT
  7. Client mosqsub/18771-master received CONNACK
  8. Client mosqsub/18771-master sending SUBSCRIBE (Mid: 1, Topic: /devices/001/temp, QoS: 0)
  9. Client mosqsub/18771-master received SUBACK
  10. Subscribed (mid: 1): 0

密码加密、加盐

上文描述的是在 MongoDB 中采用明文的方式保存密码,EMQ X 还支持用加密算法对密码进行加密和加盐处理。

  • 修改配置文件:打开配置文件 emqx_auth_mongo.conf
    • 更改配置 auth.mongo.password_hash = salt,sha256 ,采用 sha256 加密算法,加入的 salt 在密码之前;如果该配置是 sha256,salt 则表示加入的 salt 在密码之后;注意:salt 只是一个标识符,不代表使用该字符进行加盐处理
    • 更改完成后重启 EMQ X 服务。
  1. auth.mongo.auth_query.password_field = password,salt
  2. ## sha512 with salt prefix
  3. auth.mongo.password_hash = sha256,salt
  • 在 MongoDB 中存入数据,根据上一步的配置,假设该客户端设置的 salt 为 mysalt,那么加盐后的密码原文为 mysaltpublic ,读者可以通过在线的 sha512工具将密码转换为密文,并存入 MongoDB。
  1. sha512("mysaltpublic") -> c3acb78da1592319f47d15c5230071f22a9d3b23671a29c8f7b4ab92d66f39aa

打开 mongo 命令行工具。

  1. ## 先删除之前保存的认证数据
  2. > db.mqtt_user.deleteOne({ username: 'userid_001'})
  3. { "acknowledged" : true, "deletedCount" : 1 }
  4. ## 保存认证数据
  5. > db.mqtt_user.insert({ username: 'userid_001', password: 'c3acb78da1592319f47d15c5230071f22a9d3b23671a29c8f7b4ab92d66f39aa', is_superuser: false, salt: 'mysalt' })
  6. WriteResult({ "nInserted" : 1 })
  7. ## 取出相关的密码和盐
  8. > db.mqtt_user.findOne({ username: 'userid_001' })
  9. {
  10. "_id" : ObjectId("5be7aad8744a3bac99a6fd0c"),
  11. "username" : "userid_001",
  12. "password" : "c3acb78da1592319f47d15c5230071f22a9d3b23671a29c8f7b4ab92d66f39aa",
  13. "is_superuser" : false,
  14. "salt" : "mysalt"
  15. }
  • 在客户端使用 mosquitto_sub 命令来连接。
  1. # mosquitto_sub -h 10.211.55.10 -u userid_001 -P public -t /devices/001/temp -d
  2. Client mosqsub/9936-master sending CONNECT
  3. Client mosqsub/9936-master received CONNACK
  4. Client mosqsub/9936-master sending SUBSCRIBE (Mid: 1, Topic: /devices/001/temp, QoS: 0)
  5. Client mosqsub/9936-master received SUBACK
  6. Subscribed (mid: 1): 0