大家好,我是yangyang.接下来带来mqtt根本第五篇:身份认证
概述
身份认证是物联网运用的主要组成部分,可以帮助有效阻挡造孽客户真个连接。为了有更好的安全保障,理解认识多种认证机制是必要的,接下来要紧张以emqx官方供应的认证机制作为学习
X.509 是密码学里公钥证书的格式标准。X.509 标准规定了证书可以包含什么信息,并解释了记录信息的方法(数据格式)。X.509 证书里含有公钥、身份信息(比如网络主机名,组织的名称或个体名称等)和署名信息(可以是证书签发机构CA的署名,也可以是自署名)。对付一份经由可信的证书签发机构署名或者可以通过其它办法验证的证书,证书的拥有者就可以用证书及相应的私钥来创建安全的通信,对文档进行数字署名。其余除了证书本身功能,X.509还附带了证书吊销列表和用于从终极对证书进行署名的证书签发机构直到终极可信点为止的证书合法性验证算法。X.509是ITU-T标准化部门基于他们之前的ASN.1定义的一套证书标准。|参考:X.509数字证书的构造与解析 - 骑牛射雕 - 博客园

EMQX 许可客户端利用 X.509 证书进行 TLS/SSL 连接,并支持将证书信息与客户端进行绑定,以实现 X.509 证书认证。其利用与事情流程如下:
签发做事端证书,为 EMQX 启用 TLS/SSL,并设置其为双向认证。签发客户端证书,将证书与私钥文件烧录到设备中,并利用其进行 TLS/SSL 连接。客户端将在 TLS 握手阶段发送证书给做事器,以证明其身份的合法性。EMQX 收到客户真个证书后,会对证书进行验证,以确认客户真个身份。如果验证通过,做事器会连续完成 TLS 握手,建立安全连接。连接成功后,EMQX 支持将证书信息映射到客户端属性,实现证书与客户真个绑定。除此之外,还可以搭配其他运用层的认证办法,如 JWT、密码认证,实现多种认证办法的组合。
特性与上风安全性:X.509 供应了一种安全可靠的认证机制,通过利用数字证书和公钥加密技能,确保了通信的机密性、完全性和身份验证。它可以防止未经授权的设备接入网络或进行恶意操作。互操作性:X.509 是一种通用的标准,被广泛支持和采取。许多物联网设备都支持 X.509 证书的利用,这使得设备之间的认证和安全通信更加大略和可靠。可扩展性:X.509 可以支持大规模的物联网支配。它供应了灵巧的证书链和证书管理机制,可以适应繁芜的物联网环境,并支持大量设备和实体的身份验证。可信任的第三方验证:X.509 证书常日由可信任的证书颁发机构(Certificate Authority)签发,这些 CA 经由严格的安全审查和验证。设备可以利用由受信赖的 CA 签发的证书,确保其身份和证书的合法性。强大的加密算法支持:X.509 支持广泛的加密算法和密钥长度,包括常用的对称加密算法和非对称加密算法。这使得物联网设备可以利用强大的密码学算法来保护通信的安全性。灵巧的证书配置和管理:X.509 具有灵巧的证书配置和管理选项。设备可以根据需求选择适当的证书属性和扩展字段,以知足特定的物联网运用需求。此外,证书的吊销和更新也可以通过证书管理机制进行有效管理。这些特性使得 X.509 成为物联网安全的空想选择,EMQX 供应了完全的 X.509 证书认证支持,可以帮助您轻松实现物联网设备的安全接入和通信。
JWT 认证JSON Web Token (JWT) 是一种基于 Token 的认证机制。它不须要做事器来保留客户真个认证信息或会话信息。EMQX 支持基于 JWT 进行用户认证,知足用户个性化安全设置的需求。
认证事理客户端在连接要求中携带 JWT,将利用预先配置的密钥或公钥对 JWT 署名进行验证。如果用户配置了 JWKS 端点,EMQX 将通过从 JWKS 端点查询到的公钥列表对 JWT 署名进行验证。
如果署名验证成功,EMQX 会连续检讨 Claims。如果存在 iat、nbf 或 exp 等 Claims,EMQX 会主动根据这些 Claims 检讨 JWT 的合法性。之外,EMQX 也支持用户自定义的 Claims 检讨。署名验证和 Claims 检讨均通过后,EMQX 才会接管客户真个连接要求。
推举用法由于 EMQX JWT 认证器只会检讨 JWT 的署名,无法对客户端身份的合法性供应包管,因此推举用户支配一个独立的认证做事器用来为客户端颁发 JWT。
此时,客户端将首先访问该认证做事器,由该认证做事器验证客户真个身份,并为合法的客户端签发 JWT,之后客户端将利用签发的 JWT 来连接 EMQX。
由于 JWT 中的 Payload 仅仅进行了 Base64 编码,因此不建议用户在 JWT 的 Payload 中存放敏感数据。为了减少 JWT 泄露和被盗的可能,除设置合理的有效期外,还建议结合 TLS 加密来担保客户端连接的安全性。
示例demo(伪代码为主,供应思路)下图全体用意是用于仿照jwt通过账户登录(说到这里,建议前端加密登录密码)成功后得到token,而如果验证失落败,就按业务退出到上级页面就行了.那对付emqx和自己搭建的mqtt做事端该当如何对接呢,接下来,我给大家分别解释.
demo
{ "code": 0, "data": { "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJieXQiLCJpYXQiOjE3MDg5MzE0NTQsImV4cCI6MTcwODkzNTA1NH0.snGICI0eDP0CViXvb_BpOkOmZwnmFiEj-TDNx_Bw8Qg", "expire": 1708935054 }, "message": "success"}
一、 emqx JWT
假定你本地已经搭建好了emqx,那么登录后台进入客户端认证,然后添加jwt认证办法,下图展示了紧张的设置页面
个中关键字段有:
jwt来自于: 意思是基于mqtt connect,利用username 还是password字段作为比对校验token,如果是passoword,那么业务系统天生的token,就须要赋值给password加密办法:jwt常用加密算法,哀求:emqx与业务做事 都必须要设置一样的算法Secret: 密钥,哀求:emqx与业务做事 都必须要设置一样的密钥Payload: 如果emqx设置了自定义的payload,同样的业务系统也须要配置二、自建mqtt做事器
咱们用nodejs举例,在我之前的文章《nodejs搭建mqtt做事端》已经先容了几种办法,大家可以参考下.我们可以基于express或者其它api框架搭建业务做事.按照emqx的履历,我们只须要在express中实现jwt认证,然后在mqtt做事端调用express的接口完成认证.
当然,对付自建做事对接业务api,我们可以高度与自身业务结合,比如一个token只能用于一次或多次校验等.
密码认证通过密码进行身份验证,这种最大略,也是利用最多的认证办法。此时,客户端须要供应能够表明身份的凭据,例如用户名、客户端 ID 以及对应的密码并将其存储在特天命据源(数据库)中。除大略便捷的内置数据库外,EMQX 还支持通过与多类后端数据库的集成供应密码认证,包括 MySQL、PostgreSQL、MongoDB 和 Redis。
对接 HTTP API 认证通过构建业务api,对接业务平台api来实现认证,这种办法与jwt办法类似.
更多认证办法参考:认证 | EMQX 企业版文档
供应一个基于workerman/mqtt的客户端demo<?phpnamespace process;use Biz\Constants;use Biz\Iot\Service\IotService;use support\utils\ArrayToolkit;use Workerman\Worker;use Workerman\Mqtt\Client;class MqttClient extends AbstractProcess{ private $subscribes = [ '/bike/status/update', '/bike/info/update', // 单车定时发布信息数据:位置和剩余电量,美团是通过手机app GPS定位作为位置;青桔单车是通过单车定位作为位置 '/bike/lock/result', '/bike/unlock/result' ]; private $topicActionMap = [ '/bike/status/update' => 'handleStatusUpdateMessage', '/bike/info/update' => 'handleInfoUpdateMessage', '/bike/lock/result' => 'handleLockResultMessage', '/bike/unlock/result' => 'handleUnLockResultMessage', ]; / @var Client / private $client; private $connectTimestamp = 0; private $retryConnectCount = 0; private $maxRetryConnectCount = 10; private $msgMap = []; public function onWorkerStart(Worker $worker) { $mqttConfig = config('mqtt.default'); if (empty($mqttConfig['listen'])) { return; } $this->client = new Client($mqttConfig['listen']); $this->client->onConnect = [$this, 'onMqttConnect'];// $this->client->onReconnect = [$this, 'onMqttReConnect']; $this->client->onMessage = [$this, 'onMqttMessage']; $this->client->onError = [$this, 'onMqttError']; $this->client->connect(); // Channel客户端连接到Channel做事端 \Channel\Client::connect('127.0.0.1', 2206); // 订阅broadcast事宜,并注册事宜回调 \Channel\Client::on('mqtt', function ($event_data) use ($worker, $mqttConfig) { // 向当前worker进程的所有客户端广播 if (!empty($event_data['token']) && $event_data['authType'] === 'jwt') { $this->client->close(); $this->client = new Client($mqttConfig['jwt']['listen'], [ 'username' => $mqttConfig['jwt']['username'], 'password' => $event_data['token'] ]); $this->client->onConnect = [$this, 'onMqttConnect']; $this->client->onMessage = [$this, 'onMqttMessage']; $this->client->onError = [$this, 'onMqttError']; $this->client->connect(); } }); } public function onMqttConnect(Client $client) { $this->connectTimestamp = time(); foreach ($this->subscribes as $subscribe) { $client->subscribe($subscribe); } } public function onMqttReConnect() { } public function onMqttError(\Throwable $error) { $this->retryConnectCount += 1; if ($this->retryConnectCount > $this->maxRetryConnectCount) { $this->client->close(); $this->retryConnectCount = 0; echo '[MQ]连接失落败,超出重连次数', PHP_EOL; } } / @param $topic @param $content @return bool|void / public function onMqttMessage($topic, $content) { $json = json_decode($content, true); if (empty($json) || !ArrayToolkit::requireds($json, ['did', 'timestamp'])) { echo '[MQ]数据造孽,格式必须是标准json', PHP_EOL; return false; } $motorcycle = $this->getIotService()->getIotDeviceByDid($json['did']); if (empty($motorcycle)) { echo '[MQ]数据造孽,设备不存在', PHP_EOL; return false; } $now = new \DateTime(); $currentTimestamp = $now->getTimestamp() 1000; if ($currentTimestamp - $json['timestamp'] >= 1000 60 5) { echo '[MQ]收到过期数据', PHP_EOL; return false; } $uniqueKey = sprintf("%s_%s_%s", $topic, $json['did'], $json['status']); if (isset($this->msgMap[$uniqueKey]) && $json['timestamp'] <= $this->msgMap[$uniqueKey]['timestamp']) { echo '[MQ]收到重复数据', PHP_EOL; return false; } else { $this->msgMap[$uniqueKey] = $json; } echo '[MQ]吸收到主题:', $topic, ',:', $content, PHP_EOL; if (isset($this->topicActionMap[$topic]) && method_exists($this, $this->topicActionMap[$topic])) { return call_user_func([$this, $this->topicActionMap[$topic]], $motorcycle, $topic, $json); } } / 处理单车状态变革 @param $motorcycle @param string $topic @param array $json @return bool / protected function handleStatusUpdateMessage($motorcycle, string $topic, array $json) { try { $currentTotalPower = $json['totalPower'] ?? 100; $updRows = [ 'id' => $motorcycle['id'], 'totalPower' => $currentTotalPower, 'usedPower' => $currentTotalPower - $motorcycle['totalPower'], 'lastedConnTime' => $motorcycle['lastedConnTime'], 'connStatus' => $json['status'] === 'online' ? 1 : -1, 'usedStatus' => 0 ]; if ($json['status'] === 'online') { $updRows['lastedConnTime'] = intval(substr($json['timestamp'], 0, -3)); } $this->getIotService()->updateIotDevice($motorcycle['id'], $updRows); return true; } catch (\Throwable $e) { echo '[MQ]handleStatusUpdateMessage出错,', $e->getMessage(), PHP_EOL; return false; } } / 处理开锁 @param $motorcycle @param string $topic @param array $json @return bool / protected function handleLockResultMessage($motorcycle, string $topic, array $json) { try { $currentTotalPower = $json['totalPower'] ?? $motorcycle['totalPower']; $updRows = [ 'id' => $motorcycle['id'], 'totalPower' => $currentTotalPower, 'usedPower' => $currentTotalPower - $motorcycle['totalPower'], 'lastedConnTime' => $motorcycle['lastedConnTime'], 'usedStatus' => $json['status'] === 'fail' ? 0 : 1 ]; $this->getIotService()->updateIotDevice($motorcycle['id'], $updRows); echo '[MQ]handleLockResultMessage, ', $motorcycle['did'], '更新开锁信息', PHP_EOL; return true; } catch (\Throwable $e) { echo '[MQ]handleLockResultMessage, ', $motorcycle['did'], $e->getMessage(), PHP_EOL; return false; } } / 处理关锁还车 @param $motorcycle @param string $topic @param array $json @return bool / protected function handleUnLockResultMessage($motorcycle, string $topic, array $json) { try { if ($json['status'] === 'ok') { $currentTotalPower = $json['totalPower'] ?? $motorcycle['totalPower']; $updRows = [ 'id' => $motorcycle['id'], 'totalPower' => $currentTotalPower, 'usedPower' => $currentTotalPower - $motorcycle['totalPower'], 'lastedConnTime' => $motorcycle['lastedConnTime'], 'usedStatus' => 0 ]; $this->getIotService()->updateIotDevice($motorcycle['id'], $updRows); } return true; } catch (\Throwable $e) { echo '[MQ]handleUnLockResultMessage,', $e->getMessage(), PHP_EOL; return false; } } / 处理单车信息变革 @param $motorcycle @param string $topic @param array $json @return bool / protected function handleInfoUpdateMessage($motorcycle, string $topic, array $json) { try { $currentTotalPower = $json['totalPower'] ?? $motorcycle['totalPower']; $updRows = [ 'id' => $motorcycle['id'], 'totalPower' => $currentTotalPower, 'usedPower' => $currentTotalPower - $motorcycle['totalPower'], 'lastedConnTime' => $motorcycle['lastedConnTime'], 'position' => $json['position'] ?? $motorcycle['position'] ]; $this->getIotService()->updateIotDevice($motorcycle['id'], $updRows); return true; } catch (\Throwable $e) { echo '[MQ]handleInfoUpdateMessage,', $e->getMessage(), PHP_EOL; return false; } } protected function getMotorcycles() { $items = $this->getIotService()->searchDevices([ 'type' => Constants::IOT_DEVICE_TYPE_MOTORCYCLE, 'noStatus' => Constants::IOT_DEVICE_STATUS_DISABLED ], ['id' => 'DESC'], 0, PHP_INT_MAX, ['id', 'did', 'sim', 'lastedConnTime', 'totalPower', 'usedStatus', 'connStatus', 'position']); return $items; } / @return IotService / protected function getIotService() { return $this->getBiz()->service('Iot:IotService'); }}