ESP 保姆级教程玩转emqx篇③ ——认证安全之使用内置数据库(Mnesia)的密码认证

Posted 单片机菜鸟哥

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ESP 保姆级教程玩转emqx篇③ ——认证安全之使用内置数据库(Mnesia)的密码认证相关的知识,希望对你有一定的参考价值。

忘记过去,超越自己

  • ❤️ 博客主页 单片机菜鸟哥,一个野生非专业硬件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资料却不多,很多都只有官方的用户指南。

目前找到一些资料:

【Mnesia文档】1、引言
【Mnesia文档】2、概述
【Mnesia文档】3、快速开始

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 进行认证,可选值有 usernameclientid。对于 MQTT 客户端来说,分别对应 CONNECT 报文中的 UsernameClient Identifier 字段。
  • 密码加密方式
    用于指定存储密码时使用的散列算法,支持plain(明文方式,不建议)、 md5sha(正常情况下sha256够用)、bcryptpbkdf2 等。对于不同的散列算法,内置数据库密码认证器会有不同的配置要求,这个是本章重点内容。

注意:

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)。每加一,散列需要的时间就会翻倍,需要的时间越长,暴力破解的难度就越高,但相应的验证用户需要花费的时间也就越长,因此需要按照您的实际情况进行取舍。

算法高级(22)-BCrypt加密算法,号称目前最安全的算法之一

内部自己实现了随机加盐处理。使用Bcrypt,每次加密后的密文是不一样的。
对一个密码,Bcrypt每次生成的hash都不一样,那么它是如何进行校验的?

  • 虽然对同一个密码,每次生成的hash不一样,但是hash中包含了salt(hash产生过程:先随机生成salt,salt跟password进行hash);
  • 在下次校验时,从hash中取出salt,salt跟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"   // 用户密码

测试结果:

连接失败,毕竟压根不存在这个账号。我们看到这里的状态是5(MQTT_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"   // 填写一个错误的用户密码

测试结果:

连接失败,毕竟压根不存在这个账号。我们看到这里的状态是4(MQTT_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为维度。

各位同学也可以试着玩玩,做到举一反三的效果。

以上是关于ESP 保姆级教程玩转emqx篇③ ——认证安全之使用内置数据库(Mnesia)的密码认证的主要内容,如果未能解决你的问题,请参考以下文章

ESP 保姆级教程玩转emqx篇③ ——认证安全之使用内置数据库(Mnesia)的密码认证

ESP 保姆级教程玩转emqx认证篇② ——认证安全之使用内置数据库(Mnesia)的密码认证

ESP 保姆级教程玩转emqx认证篇① ——控制客户端连接,认证安全

ESP 保姆级教程玩转emqx篇② ——控制客户端连接,认证安全

ESP 保姆级教程玩转emqx数据集成篇③ ——消息重发布

ESP 保姆级教程玩转emqx MQTT篇③ ——小程序测试效果