基于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)是一种安全协议,旨在实现数据加密传输。
完整示例
源码下载
环境搭建
我们首先需要完成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.ets import 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.ets import http from '@ohos.net.http'; ... @Entry @Component struct 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支持设置回调,在回调中保存响应头数据。
// http_exec.cpp bool HttpExec::RequestWithoutCache(RequestContext *context) { if (!staticVariable_.initialized) { NETSTACK_LOGE("curl not init"); return false; } auto handle = curl_easy_init(); ... if (!SetOption(handle, context, context->GetCurlHeaderList())) { NETSTACK_LOGE("set option failed"); return false; } ... return true; } ... bool HttpExec::SetOption(CURL *curl, RequestContext *context, struct curl_slist *requestHeader) { const std::string &method = context->options.GetMethod(); if (!MethodForGet(method) && !MethodForPost(method)) { NETSTACK_LOGE("method %{public}s not supported", method.c_str()); return false; } if (context->options.GetMethod() == HttpConstant::HTTP_METHOD_HEAD) { NETSTACK_CURL_EASY_SET_OPTION(curl, CURLOPT_NOBODY, 1L, context); } // 设置请求URL NETSTACK_CURL_EASY_SET_OPTION(curl, CURLOPT_URL, context->options.GetUrl().c_str(), context); ... // 设置CURLOPT_WRITEFUNCTION传输选项,OnWritingMemoryBody为回调函数 NETSTACK_CURL_EASY_SET_OPTION(curl, CURLOPT_WRITEFUNCTION, OnWritingMemoryBody, context); NETSTACK_CURL_EASY_SET_OPTION(curl, CURLOPT_WRITEDATA, context, context); // 在OnWritingMemoryHeader写入响应头数据 NETSTACK_CURL_EASY_SET_OPTION(curl, CURLOPT_HEADERFUNCTION, OnWritingMemoryHeader, context); NETSTACK_CURL_EASY_SET_OPTION(curl, CURLOPT_HEADERDATA, context, context); ... return true; } ... #define NETSTACK_CURL_EASY_SET_OPTION(handle, opt, data, asyncContext) \ do { CURLcode result = curl_easy_setopt(handle, opt, data); \ if (result != CURLE_OK) { \ const char *err = curl_easy_strerror(result); \ NETSTACK_LOGE("Failed to set option: %{public}s, %{public}s %{public}d", #opt, err, result); \ (asyncContext)->SetErrorCode(result); \ return false; \ }
传输选项设置成功后,调用curl_multi_perform执行传输请求,并通过curl_multi_info_read查询处理句柄是否有消息返回,最后进入HandleCurlData方法处理返回数据。
// http_exec.cpp void 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加密传输。
// http_exec.cpp bool HttpExec::GetCurlDataFromHandle(CURL *handle, RequestContext *context, CURLMSG curlMsg, CURLcode result) { ... context->response.ParseHeaders(); return true; } // http_response.cpp void HttpResponse::ParseHeaders() { std::vector<std::string> vec = CommonUtils::Split(rawHeader_, HttpConstant::HTTP_LINE_SEPARATOR); for (const auto &header : vec) { if (CommonUtils::Strip(header).empty()) { continue; } auto index = header.find(HttpConstant::HTTP_HEADER_SEPARATOR); if (index == std::string::npos) { header_[CommonUtils::Strip(header)] = ""; NETSTACK_LOGI("HEAD: %{public}s", CommonUtils::Strip(header).c_str()); continue; } header_[CommonUtils::ToLower(CommonUtils::Strip(header.substr(0, index)))] = CommonUtils::Strip(header.substr(index + 1)); } }
将本篇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握手过程中的传输数据包来理解数据安全传输。