【ESP 保姆级教程】玩转emqx篇③ ——认证安全之使用内置数据库(Mnesia)的密码认证
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
忘记过去超越自己
- ❤️ 博客主页 单片机菜鸟哥一个野生非专业硬件IOT爱好者 ❤️
- ❤️ 本篇创建记录 2023-01-15 ❤️
- ❤️ 本篇更新记录 2022-01-15 ❤️
- 🎉 欢迎关注 🔎点赞 👍收藏 ⭐️留言📝
- 🙏 此博客均由博主单独编写不存在任何商业团队运营如发现错误请留言轰炸哦及时修正感谢支持
- 🔥 Arduino ESP8266教程累计帮助过超过1W+同学入门学习硬件网络编程入选过选修课程刊登过无线电杂志 🔥
- 🔥 菜鸟项目合集 🔥
快速导读
手把手代码注释完整案例讲解开发过程以及细节一键式运行代码。
ESP保姆级付费专栏群707958244
不喜勿加凭借付费专栏订单号加入
1. 前言
在前面一章 玩转emqx篇② ——控制客户端连接认证安全 中 我们介绍到认证安全有非常多的方式。
那么接下来我们针对一些常用的进行详细讲解细节。
认证安全最重要的目的就是
管理谁能连上服务器
。首次安装emqx如果没有配置任何认证安全策略所有人都可以连接上你的emqx服务器。
本章主要讲解 使用内置数据库Mnesia的密码认证
。
1.1 认证原理
密码认证通常需要由用户提供身份 ID
和对应的密码
身份 ID 用于标识用户的身份可以是用户名
、客户端标识符
或者证书通用名称
等。身份 ID 与密码的正确组合只在用户和认证系统之间共享因此认证系统可以通过比较用户提供的密码和存储在自己数据库中的密码来验证用户所声明身份的真实性。
1.2 避免存储明文密码
为了完成身份验证用户与认证系统之间需要共享一些信息例如密码。但这意味着原本应该保密的密码现在被多方持有这会显著增加密码泄漏的概率因为攻击者攻击任意一方都有可能窃取到密码
。
因此我们不建议在认证系统的数据库中以明文的形式存储密码
。因为一旦遭遇拖库这些密码将完全暴露在攻击者面前。我们更建议生成一个随机的盐
然后在数据库中存储这个盐和对密码加盐后散列得到的值。这样即便攻击者窃取到了数据库中的数据他既不能拿着这个散列值来进行登录也很难根据散列值反推出真正的密码。
Hash(密码+随机盐) 得到一个散列值HashCode。一般这个值看起来是乱码且无法反推出原始数据。
2. Mnesia数据库了解即可
Mnesia是一个分布式数据库管理系统适合于电信和其它需要持续运行和具备软实时特性的Erlang应用
越来越受关注和使用但是目前Mnesia资料却不多很多都只有官方的用户指南。
目前找到一些资料
3. 使用内置数据库Mnesia的密码认证
EMQX 支持使用内置数据库Mnesia作为客户端身份凭据的存储介质无需用户额外部署其他数据库能够做到开箱即用
。使用内置数据库也是 EMQX 的默认推荐方案因为它为身份验证提供了最佳性能
开机加载在内存中。
对于初学者来说这种方式搭建最简单高效。
使用 EMQX Dashboard 来创建使用内置数据库的密码认证。
在 Dashboard > 访问控制 > 认证 (opens new window)页面单击创建
。
3.1 操作步骤1选择认证方式为 Password-Based
3.2 操作步骤2选择数据源为 Built-in Database
3.3 操作步骤3配置参数
账号类型
用于指定 EMQX 应当使用哪个字段作为客户端的身份 ID 进行认证可选值有username
和clientid
。对于 MQTT 客户端来说分别对应 CONNECT 报文中的Username
和Client Identifier
字段。- 密码加密方式
用于指定存储密码时使用的散列算法支持plain
明文方式不建议、md5
、sha
正常情况下sha256够用、bcrypt
、pbkdf2
等。对于不同的散列算法内置数据库密码认证器会有不同的配置要求这个是本章重点内容。
注意
plain 不在考虑范围。明文存储密码。
3.3.1 配置为 md5、sha 等散列算法
加盐方式用于指定盐和密码的组合方式在密码尾部加盐password+suffix
还是在密码头部加盐prefix+password
也可以不加盐disable。
上面说到了一个叫做加盐salt
的词汇对于初学者怎么去理解它呢
一般我们会把密码经过hash编码之后得到一个散列密码值一般看起来像是乱码。
Hash(密码) = hashCode
这种方式是比较固定的如果我们在密码基础上加一个随机值salt就变成了Hash(密码+salt) = hashCode
这样只要salt稍微变化一点点整个hashcode就完全不一样。为什么叫做加盐呢可以简单理解为同样一道菜放盐多一点或者放盐少一点整个菜的味道还是完全不一样的。
头部加盐或者尾部加盐其实就是
Hash(salt+密码)以及Hash(密码+salt)。就像炒菜前面一开始就放盐还是后面才放盐味道天差地别。
对应配置文件 /var/lib/emqx/configs/cluster-override.conf
内容
authentication = [
{
backend = "built_in_database"
enable = false
mechanism = "password_based"
password_hash_algorithm {name = "sha256", salt_position = "suffix"}
user_id_type = "username"
}
]
关注password_hash_algorithm
password_hash_algorithm {
name = sha256 # plain, md5, sha, sha512
salt_position = suffix # prefix, disable
}
3.3.2 配置为 bcrypt 算法
Salt Rounds又称成本因子用于指定散列需要的计算次数
2^Salt Rounds。每加一散列需要的时间就会翻倍需要的时间越长暴力破解的难度就越高但相应的验证用户需要花费的时间也就越长
因此需要按照您的实际情况进行取舍。
内部自己实现了随机加盐
处理。使用Bcrypt每次加密后的密文是不一样的。
对一个密码Bcrypt每次生成的hash都不一样那么它是如何进行校验的
- 虽然对同一个密码每次生成的hash不一样但是hash中包含了salthash产生过程先随机生成saltsalt跟password进行hash
- 在下次校验时从hash中取出saltsalt跟password进行hash得到的结果跟保存在DB中的hash进行比对。
举个例子
加密后的格式一般为:$2a 10 10 10/bTVvqqlH9UiE0ZJZ7N2Me3RIgUCdgMheyTgV0B4cMCSokPa.6oCa
其中$是分割符无意义2a是bcrypt加密版本号10是cost的值而后的前22位是salt值再然后的字符串就是密码的密文了。
对应配置文件 /var/lib/emqx/configs/cluster-override.conf
内容
authentication {
backend = "built_in_database"
mechanism = "password_based"
password_hash_algorithm {name = "bcrypt", salt_rounds = "10"}
user_id_type = "username"
}
关注password_hash_algorithm
password_hash_algorithm {name = "bcrypt", salt_rounds = "10"}
3.3.3 配置为 pkbdf2 算法
伪随机函数
用于指定生成密钥使用的散列函数。
迭代次数
用于指定散列次数。
密钥长度
指定希望得到的密钥长度。如果未指定则表示由 伪随机函数 决定输出的密钥长度。
PBKDF2 算法概述
对应配置文件 /var/lib/emqx/configs/cluster-override.conf
内容
authentication {
backend = "built_in_database"
mechanism = "password_based"
password_hash_algorithm {
iterations = 4096
mac_fun = "sha256"
name = "pbkdf2"
}
user_id_type = "username"
}
关注password_hash_algorithm
password_hash_algorithm {
iterations = 4096
mac_fun = "sha256"
name = "pbkdf2"
}
3.4 操作步骤4用户管理
我们以密码加密方式 sha256 为例子。
当我们构建好数据库之后就可以添加管理用户。
这里我们加三个测试用户。
- 商场1号密码123456
- 商场2号密码123456
- 超级用户密码也是123456
最终结果
到这里整个配置就完成了接下来我们测试一下效果。
4. MQTTX 测试
需要创建4个连接分别是随机账号、商场1号、商场2号、超级用户。
4.1 测试随机账号
主要是填上自己的ip地址以及username。
这里为:
- username 随机账号
点击连接直接提示错误。
原因它就没有在我们的用户管理名单中
4.2 测试商场1号
主要是填上自己的ip地址以及username、正确密码。
这里为:
- username 商场1号
4.3 测试商场2号
主要是填上自己的ip地址以及username、错误密码。
这里为:
- username 商场2号
- 密码随机填
4.4 超级用户
主要是填上自己的ip地址以及username、正确密码。
这里为:
- username dpjcn
- 密码正确123456
注意
EMQX 也允许用户为某些特殊的客户端设置超级用户权限从而跳过后续所有的权限检查。(后续会讲授权管理)
5. ESP8266 测试
以下代码是emqx 认证安全测试更改一些参数之后直接烧录到nodemcu中。
/**
* 功能 emqx 认证安全测试
*
* 1、运行前提
* 这里尽量把第三方库集成在工程目录下如出现xxxx库找不到请按照下面方式进行安装。
* - 缺少 PubSubClient。 工具 -> 管理库 -> 搜索 PubSubClient -> 安装最新版本
*
* 2、逻辑描述
* - 连接上emqx服务器然后测试测试认证功能包括商品1号、商品2号、超级用户等等
*
* 3、硬件材料
* - 1*ESP8266-12 NodeMcu板子
*/
// 导入必要的库
#include <ESP8266WiFi.h> // 引入WiFi核心库
#include "PubSubClient.h" // 引入MQTT处理库
/******************* 常量声明 **********************/
#define SSID "xxxxxxx" // 填入自己的WiFi账号
#define PASSWORD "xxxxx" // 填入自己的WiFi密码
//---------------- emqx相关配置信息 ------------------//
#define MQTT_SERVER "192.168.4.1" // mqtt服务器IP地址替换为自己的
#define MQTT_PORT 1883 // mqtt服务器端口号默认是1883除非你修改过配置
#define MQTT_USERNAME "商场1号" // 用户名字
#define MQTT_PASSWORD "123456" // 用户密码
//---------------- emqx相关配置信息 ------------------//
WiFiClient espClient;
PubSubClient client(espClient);
long lastMsg = 0;
char msg[50];
int value = 0;
void setup_wifi() {
delay(10);
// We start by connecting to a WiFi network
Serial.println();
Serial.print("Connecting to ");
Serial.println(SSID);
WiFi.begin(SSID, PASSWORD);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
randomSeed(micros());
Serial.println("");
Serial.println("WiFi connected");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
}
/**
* 消息回调
*/
void callback(char* topic, byte* payload, unsigned int length) {
Serial.print("Message arrived [");
Serial.print(topic);
Serial.print("] ");
for (int i = 0; i < length; i++) {
Serial.print((char)payload[i]);
}
Serial.println();
// 根据第一个字符来控制led灯
if ((char)payload[0] == '1') {
digitalWrite(BUILTIN_LED, LOW);
} else {
digitalWrite(BUILTIN_LED, HIGH);
}
}
/**
* 断开重连
*/
void reconnect() {
// Loop until we're reconnected
while (!client.connected()) {
Serial.print("Attempting MQTT connection...");
// 创建一个随机ClientID
String clientId = "ESP8266Client-";
clientId += String(random(0xffff), HEX);
// 尝试连接emqx mqtt服务器参数clientid、username、password
if (client.connect(clientId.c_str(),MQTT_USERNAME,MQTT_PASSWORD)) {
Serial.println("connected");
// 发布一个消息主题是 outTopic
client.publish("outTopic", "hello world");
// 订阅 inTopic 主题
client.subscribe("inTopic");
} else {
Serial.print("failed, rc=");
Serial.print(client.state());
Serial.println(" try again in 5 seconds");
// 等待5s重连
delay(5000);
}
}
}
void setup() {
delay(2000); // 延时2秒用于等待系统上电稳定
pinMode(BUILTIN_LED, OUTPUT); // 初始化LED引脚为输出引脚
Serial.begin(115200); // 初始化串口波特率 115200
Serial.println(""); // 串口默认先换行显示
Serial.println("esp_emqx run~");// 串口打印信息表示项目启动~
setup_wifi(); // 初始化Wifi连接
client.setServer(MQTT_SERVER, MQTT_PORT); //配置mqtt服务器地址和端口
client.setCallback(callback); //设置订阅消息回调
}
void loop() {
//重连机制
if (!client.connected()) {
reconnect();
}
//不断监听信息
client.loop();
long now = millis();
if (now - lastMsg > 2000) {
//每2s发布一次信息
lastMsg = now;
++value;
snprintf (msg, 50, "hello world #%ld", value);
Serial.print("Publish message: ");
Serial.println(msg);
client.publish("outTopic", msg);
}
}
更改参数内容
/******************* 常量声明 **********************/
#define SSID "xxxxxxx" // 填入自己的WiFi账号
#define PASSWORD "xxxxx" // 填入自己的WiFi密码
//---------------- emqx相关配置信息 ------------------//
#define MQTT_SERVER "192.168.4.1" // mqtt服务器IP地址替换为自己的
#define MQTT_PORT 1883 // mqtt服务器端口号默认是1883除非你修改过配置
#define MQTT_USERNAME "商场1号" // 用户名字
#define MQTT_PASSWORD "123456" // 用户密码
//---------------- emqx相关配置信息 ------------------//
/******************* 常量声明 **********************/
根据提示更改即可。
接下来我们分别测试几个场景。
5.1 测试随机账号
配置改为:
#define MQTT_USERNAME "随机账号" // 用户名字
#define MQTT_PASSWORD "123456" // 用户密码
测试结果
连接失败毕竟压根不存在这个账号。我们看到这里的状态是5MQTT_CONNECT_UNAUTHORIZED 认证失败.
//状态定义
// Possible values for client.state()
#define MQTT_CONNECTION_TIMEOUT -4
#define MQTT_CONNECTION_LOST -3
#define MQTT_CONNECT_FAILED -2
#define MQTT_DISCONNECTED -1
#define MQTT_CONNECTED 0
#define MQTT_CONNECT_BAD_PROTOCOL 1
#define MQTT_CONNECT_BAD_CLIENT_ID 2
#define MQTT_CONNECT_UNAVAILABLE 3
#define MQTT_CONNECT_BAD_CREDENTIALS 4
#define MQTT_CONNECT_UNAUTHORIZED 5
/**
* 获取Mqtt客户端当前状态
*/
int PubSubClient::state() {
return this->_state;
}
5.2 测试商场1号
配置改为:
#define MQTT_USERNAME "商场1号" // 用户名字
#define MQTT_PASSWORD "123456" // 用户密码
测试结果
正确连接成功。
5.3 测试商场2号
配置改为:
#define MQTT_USERNAME "商场2号" // 用户名字
#define MQTT_PASSWORD "1234567" // 填写一个错误的用户密码
测试结果
连接失败毕竟压根不存在这个账号。我们看到这里的状态是4MQTT_CONNECT_BAD_CREDENTIALS 错误的认证信息一般就是某一个信息不对.
//状态定义
// Possible values for client.state()
#define MQTT_CONNECTION_TIMEOUT -4
#define MQTT_CONNECTION_LOST -3
#define MQTT_CONNECT_FAILED -2
#define MQTT_DISCONNECTED -1
#define MQTT_CONNECTED 0
#define MQTT_CONNECT_BAD_PROTOCOL 1
#define MQTT_CONNECT_BAD_CLIENT_ID 2
#define MQTT_CONNECT_UNAVAILABLE 3
#define MQTT_CONNECT_BAD_CREDENTIALS 4
#define MQTT_CONNECT_UNAUTHORIZED 5
/**
* 获取Mqtt客户端当前状态
*/
int PubSubClient::state() {
return this->_state;
}
5.4 测试超级用户
配置改为:
#define MQTT_USERNAME "dpjcn" // 用户名字
#define MQTT_PASSWORD "123456" // 用户密码
测试结果
正确连接成功。
6. 总结
使用内置数据库也是 EMQX 的默认推荐方案因为它为身份验证提供了最佳性能
开机加载在内存中。建议初学者可以先用这种方式去操作。
思考题
上面的方式是以username为维度其实在选择字段的时候也可以以clientid为维度。
各位同学也可以试着玩玩做到举一反三的效果。