• 基于 HarmonyOS 的 HTTPS 请求过程开发示例(ArkTS)


    介绍

    本篇 Codelab 基于网络模块以及 Webview 实现一次 HTTPS 请求,并对其过程进行抓包分析。效果如图所示:

    相关概念

    ● Webview:提供 Web 控制能力,Web 组件提供网页显示能力。

    ● HTTP数据请求:网络管理模块,提供 HTTP 数据请求能力,支持 GET、POST、OPTIONS、HEAD、PUT、DELETE、TRACE、CONNECT 请求方法。

    ● HTTPS:应用层协议,支持加密传输以及身份认证,保证数据的安全传输。

    ● SSL:SSL(Secure Socket Layer)安全套接层是位于传输通信协议(TCP/IP)之上实现的一种安全协议。

    ● TLS:TLS(Transport Layer Security)是一种安全协议,旨在实现数据加密传输。

    完整示例

    gitee源码地址

    源码下载

    HTTPS请求过程(ArkTS).zip

    环境搭建

    我们首先需要完成 HarmonyOS 开发环境搭建,可参照如下步骤进行。

    软件要求

    ● DevEco Studio版本:DevEco Studio 3.1 Release。 

    ● HarmonyOS SDK版本:API version 9。

    硬件要求

    ● 设备类型:华为手机或运行在 DevEco Studio 上的华为手机设备模拟器。

    ● HarmonyOS 系统:3.1.0 Developer Release。

    环境搭建

    1.  安装 DevEco Studio,详情请参考下载和安装软件

    2.  设置 DevEco Studio 开发环境,DevEco Studio 开发环境需要依赖于网络环境,需要连接上网络才能确保工具的正常使用,可以根据如下两种情况来配置开发环境:

    ● 如果可以直接访问 Internet,只需进行下载HarmonyOS SDK操作。

    ● 如果网络不能直接访问 Internet,需要通过代理服务器才可以访问,请参考配置开发环境

    3.  开发者可以参考以下链接,完成设备调试的相关配置:

    ● 使用真机进行调试

    ● 使用模拟器进行调试

    代码结构解读

    本篇 Codelab 只对核心代码进行讲解,对于完整代码,我们会在源码下载或 gitee 中提供。

    ├──entry/src/main/ets                // 代码区│  ├──common│  │  ├──constants│  │  │  ├──StyleConstants.ets       // 样式常量类 │  │  │  └──CommonConstants.ets      // 常量类│  │  └──utils│  │     ├──HttpUtil.ets             // 网络请求方法│  │     └──Logger.ets               // 日志打印工具类│  ├──entryability│  │  └──EntryAbility.ts             // 程序入口类│  └──pages│     └──WebPage.ets                 // 页面入口└──entry/src/main/resources          // 资源文件目录
    
    

    创建 HTTPS 请求

    HTTPS 协议是位于应用层的一种安全传输协议,与 HTTP 最大的区别是服务端与客户端之间进行数据传输都会经过 TLS/SSL 加密。该示例请求HarmonyOS官网,并将请求得到的内容通过 Web 容器展示出来。效果如图所示:

    首先在 HttpUtil.ets 中调用 createHttp 方法创建一个请求任务,再通过 request 方法发起网络请求。该方法支持三个参数:url、options 以及 callback 回调,其中 options 可以设置请求方法、请求头以及超时时间等。

    // HttpUtil.etsimport http from '@ohos.net.http';export default async function httpGet(url: string) {  if (!url) {    return undefined;  }  let request = http.createHttp();  let options = {    method: http.RequestMethod.GET,    header: { 'Content-Type': 'application/json' },    readTimeout: CommonConstant.READ_TIMEOUT,    connectTimeout: CommonConstant.CONNECT_TIMEOUT  } as http.HttpRequestOptions;  let result = await request.request(url, options);  return result;}
    
    

    接着在入口页面中调用上述封装的 httpGet 方法请求指定网址,将请求得到的内容嵌入到 Web 组件中。

    // WebPage.etsimport http from '@ohos.net.http';...@Entry@Componentstruct WebPage {  @State webVisibility: Visibility = Visibility.Hidden;  ...  build() {    Column() {      ...    }  }
      async onRequest() {    if (this.webVisibility === Visibility.Hidden) {      this.webVisibility = Visibility.Visible;      try {        let result = await httpGet(this.webSrc);        if (result && result.responseCode === http.ResponseCode.OK) {          this.controller.clearHistory();          this.controller.loadUrl(this.webSrc);        }       } catch (error) {        promptAction.showToast({          message: $r('app.string.http_response_error')        })      }    } else {      this.webVisibility = Visibility.Hidden;    }  }}
    
    

    分析模块源码可知,通过 request 方法建立请求后,模块底层首先会调用三方库libcurl中的 curl_easy_init 初始化一个简单会话。初始化完成后,接着调用 curl_easy_setopt 方法设置传输选项。其中 CURLOPT_URL 用于设置请求的 URL 地址,对应 request 中的 url 参数;CURLOPT_WRITEFUNCTION 可以设置一个回调,保存接收的数据;CURLOPT_HEADERDATA 支持设置回调,在回调中保存响应头数据。

    1. // http_exec.cpp
    2. bool HttpExec::RequestWithoutCache(RequestContext *context)
    3. {
    4. if (!staticVariable_.initialized) {
    5. NETSTACK_LOGE("curl not init");
    6. return false;
    7. }
    8.     auto handle = curl_easy_init();
    9. ...
    10. if (!SetOption(handle, context, context->GetCurlHeaderList())) {
    11. NETSTACK_LOGE("set option failed");
    12. return false;
    13. }
    14. ...
    15. return true;
    16. }
    17. ...
    18. bool HttpExec::SetOption(CURL *curl, RequestContext *context, struct curl_slist *requestHeader)
    19. {
    20. const std::string &method = context->options.GetMethod();
    21. if (!MethodForGet(method) && !MethodForPost(method)) {
    22. NETSTACK_LOGE("method %{public}s not supported"method.c_str());
    23. return false;
    24. }
    25. if (context->options.GetMethod() == HttpConstant::HTTP_METHOD_HEAD) {
    26. NETSTACK_CURL_EASY_SET_OPTION(curl, CURLOPT_NOBODY, 1L, context);
    27. }
    28. // 设置请求URL
    29. NETSTACK_CURL_EASY_SET_OPTION(curl, CURLOPT_URL, context->options.GetUrl().c_str(), context);
    30. ...
    31. // 设置CURLOPT_WRITEFUNCTION传输选项,OnWritingMemoryBody为回调函数
    32. NETSTACK_CURL_EASY_SET_OPTION(curl, CURLOPT_WRITEFUNCTION, OnWritingMemoryBody, context);
    33. NETSTACK_CURL_EASY_SET_OPTION(curl, CURLOPT_WRITEDATA, context, context);
    34. // 在OnWritingMemoryHeader写入响应头数据
    35. NETSTACK_CURL_EASY_SET_OPTION(curl, CURLOPT_HEADERFUNCTION, OnWritingMemoryHeader, context);
    36. NETSTACK_CURL_EASY_SET_OPTION(curl, CURLOPT_HEADERDATA, context, context);
    37. ...
    38. return true;
    39. }
    40. ...
    41. #define NETSTACK_CURL_EASY_SET_OPTION(handle, opt, data, asyncContext)                                   \
    42. do {
    43.         CURLcode result = curl_easy_setopt(handle, opt, data);                                           \
    44. if (result != CURLE_OK) {                                                                        \
    45. const char *err = curl_easy_strerror(result);                                                \
    46. NETSTACK_LOGE("Failed to set option: %{public}s, %{public}s %{public}d", #opt, err, result); \
    47. (asyncContext)->SetErrorCode(result);                                                        \
    48. return false;                                                                                \
    49. }                                                                                                \

    传输选项设置成功后,调用 curl_multi_perform 执行传输请求,并通过 curl_multi_info_read 查询处理句柄是否有消息返回,最后进入 HandleCurlData 方法处理返回数据。

    // http_exec.cppvoid HttpExec::SendRequest(){    ...    do {        ...        auto ret = curl_multi_perform(staticVariable_.curlMulti, &runningHandle);        ...    } while (runningHandle > 0);}...void HttpExec::ReadResponse(){    CURLMsg *msg = nullptr; /* NOLINT */    do {        ...        msg = curl_multi_info_read(staticVariable_.curlMulti, &leftMsg);        if (msg) {            if (msg->msg == CURLMSG_DONE) {                HandleCurlData(msg);            }        }    } while (msg);}
    
    

    在 HandleCurlData 函数中调用 ParseHeaders 函数将上面回调写入的响应头解析出来,其中响应头中会携带客户端和服务端支持的最高网络协议,如果是 HTTP/2 表示支持 HTTPS 加密传输。

    1. // http_exec.cpp
    2. bool HttpExec::GetCurlDataFromHandle(CURL *handle, RequestContext *context, CURLMSG curlMsg, CURLcode result)
    3. {
    4. ...
    5.     context->response.ParseHeaders();
    6. return true;
    7. }
    8. // http_response.cpp
    9. void HttpResponse::ParseHeaders()
    10. {
    11.     std::vector<std::string> vec = CommonUtils::Split(rawHeader_, HttpConstant::HTTP_LINE_SEPARATOR);
    12. for (const auto &header : vec) {
    13. if (CommonUtils::Strip(header).empty()) {
    14. continue;
    15. }
    16.         auto index = header.find(HttpConstant::HTTP_HEADER_SEPARATOR);
    17. if (index == std::string::npos) {
    18.             header_[CommonUtils::Strip(header)] = "";
    19. NETSTACK_LOGI("HEAD: %{public}s", CommonUtils::Strip(header).c_str());
    20. continue;
    21. }
    22.         header_[CommonUtils::ToLower(CommonUtils::Strip(header.substr(0index)))] =
    23.             CommonUtils::Strip(header.substr(index + 1));
    24. }
    25. }

    将本篇 Codelab 中的网址协议头更改为 http 时,在 DevEco Studio 的日志中看到服务端会返回 301 状态码永久重定向到 https,因此最终通信依旧会经历 TLS 加密传输。

    模块源码可以在 Gitee 开源仓库 communication_netstack 中获取,本篇 Codelab 引用源码部分位于 http_exec 文件中。

    TLS/SSL 握手过程

    本章节主要通过抓包数据分析 TLS 协议的握手过程,其中包括交换参数、证书验证、密钥计算以及验证密钥等,抓包内容如图所示:

    握手过程如图所示:

    5.1 第一次握手

    根据上图中可以看到,客户端首先会进行第一次握手连接,发送“Client Hello”消息给服务端开启一个新的会话连接。分析数据包得到,客户端在第一次握手时会向服务端传递协议版本号(TLS1.2)、随机数(Client Random,用于后续生成“会话密钥”)、Session ID 以及 Cipher Suites(客户端支持的密码套件)。数据内容如图所示:

    5.2 第二次握手

    服务端接收到客户端数据后,将响应数据通过“Sever Hello”传递给客户端,包括随机数(Sever Random,用于后续生成“会话密钥”)、协议版本号(TLS1.2)以及 Cipher Suite(任意选择一个客户端支持的密码套件),数据内容如图所示:

    服务端传递“Sever Hello”后,紧跟着会将 Certificate(证书)、“Sever Key Exchange”消息以及“Server Hello Done”消息传递给客户端。此处着重分析“Sever Key Exchange”,数据内容如图所示:

    5.3 第三次握手

    客户端收到“Server Hello Done”消息后,会将 Client Params 数据传递给服务端,其中包含自身生成的椭圆曲线公钥(Pubkey),数据内容如图所示:

    经过上述过程,客户端持有 Client Random、Server Random 以及 Server Params,将 Server Params 使用服务端公钥解密后得到“Server Key Exchange”消息中的临时公钥,客户端使用 x25519 算法计算出预主密钥(Premaster Secret),然后再结合客户端随机数、服务端随机数以及预主密钥生成主密钥,最终构建“会话密钥”。“Change Cipher Spec”消息表示客户端已经生成密钥,并切换到加密模式。最后将之前所有的握手数据做一个摘要,再利用双方协商好的对称密钥进行加密, 通过“Encrypted Handshake Message”消息将加密数据传递给服务端做校验。数据内容如图所示:

    5.4 第四次握手

    服务端利用 Client Random、Server Random 以及 Client Params 计算得出“会话密钥”,向客户端传递“Change Cipher Spec”和“Encrypted Handshake Message”消息供客户端校验。当双方校验通过后,真正的数据才开始传输。

    总结

    您已经完成了本次 Codelab 的学习,并了解到以下知识点:

    1.  使用 @ohos.net.http 建立一次 https 请求。

    2.  通过分析 TLS/SSL 握手过程中的传输数据包来理解数据安全传输。

  • 相关阅读:
    基于Java原生实现汽修管理系统《建议收藏:附完整源码+数据库》
    分布式事务解决方案
    洛谷 T284656 改试卷(paper)
    Vue2.0+AntvX6—边 Edge
    基于Open3D的点云处理18-重建系统
    3天快速入门python机器学习(黑马xxx)
    ImportError: cannot import name ‘secure_write‘ from ‘jupyter_core.paths‘解决方案
    Python 的运算符和语句(条件、循环、异常)基本使用指南
    mindspore提示Syntax error. Invalid data, Page size: 33554432 is too small
    【分布式】入门级NCCL多机并行实践 - 02
  • 原文地址:https://blog.csdn.net/HarmonyOSDev/article/details/134313306