Flutter https双向认证

2,570 阅读2分钟

       最近公司的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//生成客户端私钥

将生成的两个文件分别通过useCertificateChainBytesusePrivateKeyBytes分别设置,客户端的私钥密码也由运维提供。

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;
}