最近有个项目需要用到MQTT协议,且客户端需要连接到公网部署的Broker,直接裸奔肯定是不可取的了,至少需要做个单向认证,折腾了将近半个工作日,期间也遇到几个坑,这里做个记录。
环境
先来说说环境。
使用的操作系统是CentOS7,因为上线生产环境也是选的CentOS7,所以直接在CentOS7进行测试配置有问题也好处理。
除了CentOS7之外,要配置MQTT Broker肯定需要对应的服务器了,服务器我这边选的是EPEL源里面自带的mosquitto,可以通过如下命令安装:
yum install mosquitto
安装成功之后,其配置文件目录在/etc/mosquitto/,主配置文件是mosquitto.conf。
为mosquitto生成自签名证书
生成所需要的证书文件步骤,请移步这里:openssl ssl证书相关命令,参考其中的利用OpenSSL生成自签名证书部分。
生成过程很简单,只说几句需要注意的:
- 由于是物联网设备使用,建议生成的证书有效期适当长久一些。
- 签发server.crt这一步尤其要注意Common Name (eg, your name or your server’s hostname) []这一栏,我就是因为之前想为IP直接申请证书,导致后续客户端连不上Broker。
- 虽然稍加折腾就能为IP签发证书,但是考虑后期服务器端运维、负载均衡等,建议客户端使用域名解析IP的方式连接。
如果过程没问题,应该会有如下几个文件:ca.crt ca.key ca.srl server.crt server.csr server.key。
配置Mosquitto单向认证
生成相关证书之后,下一步就是配置mosquitto启用TLS了。
mosquitto默认监听在1883端口,无加密无认证,只需要IP及端口号就能随意连接,如果不需要可以删除默认的配置,我这边默认不改变这部分内容配置。
编辑/etc/mosquitto/mosquitto.conf,在文件末尾增加如下内容:
port 8883
cafile /etc/mosquitto/certs/ca.crt
keyfile /etc/mosquitto/certs/server.key
certfile /etc/mosquitto/certs/server.crt
tls_version tlsv1
单向认证配置很简单,就是指定端口、证书文件及TLS版本,实际配置需要注意证书文件的存放位置。
测试配置文件
更改配置文件之后,最好先测试一下配置文件,确保配置指令都正确再重启服务端,相关命令如下:
[root@cloudbool ~]# mosquitto -c /etc/mosquitto/mosquitto.conf
1562146147: mosquitto version 1.5.8 starting
1562146147: Config loaded from /etc/mosquitto/mosquitto.conf.
1562146147: Opening ipv4 listen socket on port 8883.
1562146147: Opening ipv6 listen socket on port 8883.
如果配置文件正确,输出应该类似如上,且服务端应该在对应的端口监听请求。
防火墙放行
如果系统防火墙处于开启状态,还需要在防火墙上放行指定的端口,以上面定的8883端口为例:
firewall-cmd --permanent --add-port=8883/tcp
firewall-cmd --reload
客户端连接
配置完服务端之后,可以测试一下客户端的连接情况。我这里使用MQTTBox作为MQTT客户端进行测试。
只需将上述生成的ca.crt复制到客户端,MQTT相关配置截图如下:
如果配置没问题,到这一步,客户端应该可以正确连接上Broker了。
PHP连接TLS加密的mosquitto的Broker
业务代码来说,我测试了两个库,都能正确连接上通过TLS加密的mosquitto的Broker,代码地址如下:
https://github.com/karpy47/PhpMqttClient
https://github.com/walkor/mqtt
Laravel下workerman连接MQTT Broker
首先,使用composer安装workerman的对应依赖包,然后创建一个命令行命令,两个步骤对应的命令如下:
composer require workerman/mqtt
php artisan make:command mqtt
接下来编辑app/Console/Commands/mqtt.php,handle方法部分代码如下:
public function handle()
{
global $argv;
$arg = $this->argument('action');
$argv [1] = $arg;
$worker = new Worker();
$worker->count = 1;
$worker->onWorkerStart = function () {
$mqtt = new \Workerman\Mqtt\Client("mqtt://" . env('MQTT_HOST') . ":" . env('MQTT_PORT'), array(
'ssl' => array(
'cafile' => '/path/mqtt/ca.crt',
'verify_peer' => false,
'allow_self_signed' => true,
),
'username' => env('MQTT_USER'),
'password' => env('MQTT_PASSWORD'),
'debug' => env('MQTT_DEBUG'),
'client_id' => $this->client_id,
'will' => [
'topic' => 'status/' . $this->client_id,
'content' => 0,
'qos' => 2,
'retain' => true,
]
));
$mqtt->transport = 'ssl';
$mqtt->onConnect = function ($mqtt) {
$mqtt->subscribe('/iot/#');
};
$mqtt->onMessage = function ($topic, $data, $mqtt) {
//some code....
};
$mqtt->connect();
};
Worker::runAll();
}
如果一切正常,就可以在命令行通过artisan命令来连接并监听对应的topic:
php artisan mqtt start
实际使用来说,因为我们后端框架使用的是Laravel,如果需要在命令行开启两个进程的话,会报错,这问题搜遍了google也无解:
Workerman[artisan] start in DEBUG mode
Workerman[artisan] already running
到目前为止,如果想同时启用两个进程,我的解决办法是再启用一个新的MQTT库,也就是上面列出的两个中的另一个。
配置过程相关报错
报错1:
openssl verify -CAfile ca.crt server.crt
C = AU, ST = Some-State, O = Internet Widgits Pty Ltd, CN = mqtt.cloudbool.com
error 18 at 0 depth lookup: self signed certificate
error server.crt: verification failed
如果出现这个问题,可能是在生成自签名证书时,两个Common Name重复了,解决办法是生成过程输入不同的内容到Common Name。
报错2:
1562125450: New connection from 106.6.xxx.xxx on port 8883.
1562125451: Socket error on client <unknown>, disconnecting.
[106.52.xxx.xxx]:188 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJeNn0rtOBYwvV5VuV895RT7GpXsXKEjt5q2hnQS17OMCbee0a0FIdYkQERLMoW3Qo1J25MvX95/z1By/vWgF1w=
1562125452: New connection from 106.6.xxx.xxx on port 8883.
1562125452: Socket error on client <unknown>, disconnecting.
这种错误,请检查签发证书过程中Common Name是否输入的是IP或者主机名。最简单的解决办法是,使用域名代替IP或者主机名进行证书签发。