最近公司的Flutter项目在做安全性优化,需要实现https双向认证,项目中的网络请求是基于dio网络请求封装的的工具类,在网上看了一些资料,大部分都说都模棱两可,没有完整的代码来解决问题,现在记录一下实现过程。
1.初始化SucuretyContext
需要实现三个方法setTrustedCertificatesBytes、useCertificateChainBytes、usePrivateKeyBytes
void initSecureContext() async{
Constant.secureContext = SecurityContext.defaultContext;
try {
Constant.secureContext.setTrustedCertificatesBytes(utf8.encode(await rootBundle.loadString("assets/certificate/server.pem")));
Constant.secureContext.useCertificateChainBytes(utf8.encode(await rootBundle.loadString("assets/certificate/client.pem")));
Constant.secureContext.usePrivateKeyBytes(utf8.encode(await rootBundle.loadString("assets/certificate/client.key")),password: '123456');
debugPrint("init securety context success");
} on Exception catch (e) {
debugPrint("SecurityContext set error : " + e.toString());
}
}
setTrustedCertificatesBytes:设置服务器证书,其中包含服务器公钥;
useCertificateChainBytes:设置客户端证书到证书链;
usePrivateKeyBytes :设置客户端私钥;
运维提供给客户端两个文件,一个server.crt 和client.p12,pem格式文件可以根据命令从crt格式转化,注意文件路径要写对,转化命令如下:
openssl x509 -in www.xx.com.crt -out www.xx.com.pem
.p12证书既包含公钥也包含私钥,可以根据如下命令得到client.pem和client.key。
openssl pkcs12 -clcerts -nokeys -out client.pem -in client.p12//生成客户端证书
openssl pkcs12 -in client.p12 -nocerts -nodes -out client.key//生成客户端私钥
将生成的两个文件分别通过useCertificateChainBytes和usePrivateKeyBytes分别设置,客户端的私钥密码也由运维提供。
2.设置httpClient的context,在创建dio对象时进行设置。
(_dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
HttpClient httpClient = new HttpClient(context: Constant.secureContext);
return httpClient;
};
基本的设置工作就完成了,但是还存在一些问题。
1.pem文件都放在assets目录中,容易被破解。
现在能想到的方案是,将pem文本转换成byte数组,然后转化成一张普通的图片,存储在images目录中,从而隐藏pem文件,如果还想做的更安全的话,可以再对生成的图片进行处理。
2.安全证书有效时间都是有限制的,一般服务端证书有效期是两年,如果只是把证书放在app中,那么到一年半左右的时间就需要考虑更新app版本,如果想动态更换证书的话,需要后端进行配合,由服务器端提供一个用于获取证书的域名+接口,当需要更新证书时,服务器端返回一张处理过的图片,转成二进制进行加载,也可以返回加密的pem文本,解密保存在app缓存目录中,调用setTrustedCertificates、useCertificateChain、usePrivateKey 直接加载文件。下面的代码是将assets目录中的图片写入目录中,生产环境可以替换为从服务器获取。
/// 获取证书的本地路径
Future<String> _getLocalFile(String filename,
{bool deleteExist: false}) async {
String dir = (await getApplicationDocumentsDirectory()).path;
debugPrint('dir = $dir');
File file = new File('$dir/$filename');
bool exist = await file.exists();
debugPrint('exist = $exist');
if (deleteExist) {
if (exist) {
file.deleteSync();
}
exist = false;
}
if (!exist) {
String data = await rootBundle.loadString("assets/certificate/" + filename);
await file.writeAsString(data);
}
debugPrint("------- " + file.path);
return file.path;
}