转载请以链接形式标明出处: 本文出自:103style的博客
修正记录: 2019/11/05 13:12 : 修改证书验证内容,目前双向验证还有问题 flutter issues 44164 2019/11/05 17:26 : 修改证书验证内容,处理双向验证失败的问题。
目录
- 遇到的相关报错信息
- 环境
- 集成过程
- 证书验证
遇到的相关报错信息
代码语言:javascript复制Unhandled Exception: FileSystemException: Cannot open file, path = '...'
(OS Error: No such file or directory, errno = 2)
TlsException: Failure trusting builtin roots
SocketException: OS Error: Connection reset by peer, errno = 104
环境
flutter doctor -v
代码语言:javascript复制>flutter doctor -v
[√] Flutter (Channel stable, v1.9.1 hotfix.5, on Microsoft Windows [Version 10.0.17134.1006], locale zh-CN)
• Flutter version 1.9.1 hotfix.5 at D:flutter
• Framework revision 1aedbb1835 (2 weeks ago), 2019-10-17 08:37:27 -0700
• Engine revision b863200c37
• Dart version 2.5.0
[√] Android toolchain - develop for Android devices (Android SDK version 28.0.3)
• Android SDK at D:Androidsdk
• Android NDK location not configured (optional; useful for native profiling support)
• Platform android-29, build-tools 28.0.3
• Java binary at: D:AndroidAndroidStudiojrebinjava
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b03)
• All Android licenses accepted.
[√] Android Studio (version 3.5)
• Android Studio at D:AndroidAndroidStudio
• Flutter plugin version 40.2.2
• Dart plugin version 191.8593
• Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b03)
集成过程
首先来到 flutter package 这个 flutter 相关的库网站,然后搜了下 mqtt,找到 mqtt_client 这个库。 上面有提供以下没有安全认证的使用示例。 原示例地址:https://pub.flutter-io.cn/packages/mqtt_client#-example-tab-
代码语言:javascript复制import 'dart:async';
import 'dart:io';
import 'package:mqtt_client/mqtt_client.dart';
///服务器地址是 test.mosquitto.org , 端口默认是1883
///自定义端口可以调用 MqttClient.withPort(服务器地址, 身份标识, 端口号);
final MqttClient client = MqttClient('test.mosquitto.org', '');
Future<int> main() async {
client.logging(on: false);///是否开启日志
client.keepAlivePeriod = 20;///设置超时时间
client.onDisconnected = onDisconnected;//设置断开连接的回调
client.onConnected = onConnected;//设置连接成功的回调
client.onSubscribed = onSubscribed;//订阅的回调
client.pongCallback = pong;//ping的回调
try {
await client.connect(); ///开始连接
} on Exception catch (e) {
print('EXAMPLE::client exception - $e');
client.disconnect();
}
///检查连接结果
if (client.connectionStatus.state == MqttConnectionState.connected) {
print('EXAMPLE::Mosquitto client connected');
} else {
print('EXAMPLE::ERROR Mosquitto client connection failed - disconnecting, status is ${client.connectionStatus}');
client.disconnect();
exit(-1);
}
///订阅一个topic: 服务端定义的事件 当服务器发送了这个消息,就会在 client.updates.listen 中收到
const String topic = 'test/lol';
client.subscribe(topic, MqttQos.atMostOnce);
///监听服务器发来的信息
client.updates.listen((List<MqttReceivedMessage<MqttMessage>> c) {
final MqttPublishMessage recMess = c[0].payload;
///服务器返回的数据信息
final String pt = MqttPublishPayload.bytesToStringAsString(recMess.payload.message);
print( 'EXAMPLE::Change notification:: topic is <${c[0].topic}>, payload is <-- $pt -->');
});
///设置public监听,当我们调用 publishMessage 时,会告诉你是都发布成功
client.published.listen((MqttPublishMessage message) {
print('EXAMPLE::Published notification:: topic is ${message.variableHeader.topicName}, with Qos ${message.header.qos}');
});
///发送消息给服务器的示例
const String pubTopic = 'Dart/Mqtt_client/testtopic';
final MqttClientPayloadBuilder builder = MqttClientPayloadBuilder();
builder.addString('Hello from mqtt_client');///这里传 请求信息的json字符串
client.publishMessage(pubTopic, MqttQos.exactlyOnce, builder.payload);
///解除订阅
client.unsubscribe(topic);
///断开连接
client.disconnect();
return 0;
}
void onSubscribed(String topic) {
print('EXAMPLE::Subscription confirmed for topic $topic');
}
void onDisconnected() {
print('EXAMPLE::OnDisconnected client callback - Client disconnection');
if (client.connectionStatus.returnCode == MqttConnectReturnCode.solicited) {
print('EXAMPLE::OnDisconnected callback is solicited, this is correct');
}
exit(-1);
}
void onConnected() {
print('EXAMPLE::OnConnected client callback - Client connection was sucessful');
}
void pong() {
print('EXAMPLE::Ping response client callback invoked');
}
然后我就按这个示例跑了以下,提供的这个测试服务器是可以连接成功的。
证书验证
但是我这边服务器做了证书验证,需要配置证书,然后就找到 mqtt_client 这个库的github地址.
然后在 issue 107 中发现 作者有提供配置证书的示例。 示例地址: https://github.com/shamblett/mqtt_client/blob/master/example/iot_core.dart
作者在 /example/pem 这个目录下提供了一个证书的文件,
然后通过 flutter 提供的 context.setTrustedCertificates(filepath)
设置证书。主要逻辑如下:
import 'dart:async';
import 'dart:io';
import 'package:path/path.dart' as path;
import 'package:mqtt_client/mqtt_client.dart';
Future<int> main() async {
...
client.secure = true;
final String currDir =
'${path.current}${path.separator}example${path.separator}';
final SecurityContext context = SecurityContext.defaultContext;
context.setTrustedCertificates(currDir path.join('pem', 'roots.pem'));
client.securityContext = context;
client.setProtocolV311();
await client.connect();
...
return 0;
}
然后跑起来就发现了第一个问题:
代码语言:javascript复制Unhandled Exception: FileSystemException: Cannot open file, path = '...'
(OS Error: No such file or directory, errno = 2)
然后我就在 issue 107 下问了这个库的作者,issue 那里可以看到我们的对话,库的作者最后说时 flutter 的 不支持 //crt/crt/cilent.crt
这种路径的访问。
我也尝试了 通过配置 assets 来访问,但是也没有相应获取路径的方法。
然后我就来到 flutter 的 github 地址那提了这个 issue:flutter/issues/43472,然而到目前 2019/11/01 16:30
为止,flutter 开发人员并没有提供相关的解决方案。
然后,最后我就想,即然读不了工程里面的文件,我就先写到手机文件系统中去,然后再获取这个文件的路径。 参考官方的 文件读写教程. 如下:
代码语言:javascript复制/// 获取证书的本地路径
Future<String> _getLocalFile(String filename,
{bool deleteExist: false}) async {
String dir = (await getApplicationDocumentsDirectory()).path;
log('dir = $dir');
File file = new File('$dir/$filename');
bool exist = await file.exists();
log('exist = $exist');
if (deleteExist) {
if (exist) {
file.deleteSync();
}
exist = false;
}
if (!exist) {
log("MqttUtils: start write cert in local");
await file.writeAsString(mqtt_cert);///mqtt_cert 为证书里面对应的内容
}
return file.path;
}
更新于 2019/11/05 17:26 START
然后修改连接的代码为:
代码语言:javascript复制_client.secure = true;
final SecurityContext context = SecurityContext.defaultContext;
String caPath =
await _getLocalFile("ca.pem", cert_ca, deleteExist: deleteExist);
String clientKeyPath = await _getLocalFile("clientkey.pem", cert_client_key,
deleteExist: deleteExist);
String clientCrtPath = await _getLocalFile("client.pem", cert_client_crt,
deleteExist: deleteExist);
try {
context.setTrustedCertificates(caPath);
context.useCertificateChain(clientCrtPath);
context.usePrivateKey(clientKeyPath);
} on Exception catch (e) {
//出现异常 尝试删除本地证书然后重新写入证书
log("SecurityContext set error : " e.toString());
return -1;
}
_client.securityContext = context;
_client.setProtocolV311();
上面代码的几个字符串分别代表: cert_ca:根证书的内容 cert_client_key:客户端私钥的内容 cert_client_crt:客户端证书的内容
更新于 2019/11/05 17:26 END
证书内容不对的话会报以下错误:
代码语言:javascript复制TlsException: Failure trusting builtin roots
更新于 2019/11/05 13:12 START
SecurityContext 有提供直接也内容的方法,并不一定要传路径…
也可以在配置 assets 通过以下方法读取内容,
代码语言:javascript复制String cerData = await rootBundle.loadString("assets/cc.pem");
utf8.encode(caPath);
然后调用以下带 bytes 的方法即可。
代码语言:javascript复制void usePrivateKey(String file, {String password});
void usePrivateKeyBytes(List<int> keyBytes, {String password});
void setTrustedCertificates(String file, {String password});
void setTrustedCertificatesBytes(List<int> certBytes, {String password});
void useCertificateChain(String file, {String password});
void useCertificateChainBytes(List<int> chainBytes, {String password});
void setClientAuthorities(String file, {String password});
void setClientAuthoritiesBytes(List<int> authCertBytes, {String password});
更新于 2019/11/05 13:12 END
然后好好的用了几天,昨天下午连接的时候突然又连接不上了!!! 报错提示:
代码语言:javascript复制SocketException: OS Error: Connection reset by peer, errno = 104
然后搜索了一圈,又去问 mqtt_client 库的作者… mqtt_client/issues/131. 然后他也没遇到过。
最后通过 Wireshark 抓包发现报错信息 TLSv1.2 Handshake failure
, 然后通过服务端哪些查看报错信息,然后搜索一圈发现可能是 docker 1.6.3版本 官方镜像的问题,也可能是昨天下午服务端同事改了配置重启之后导致的,感觉应该是后者…
最后发现是自己证书配置的问题! 上面的代码示例 和 demo中的已修正! 之前能连上是因为服务端没有配置双向验证。
最后提供一个Demo: https://github.com/103style/mqtt_demo
以上
如果觉得不错的话,请帮忙点个赞呗。