介紹
本篇 Codelab 基于網(wǎng)絡(luò)模塊以及 Webview 實現(xiàn)一次 HTTPS 請求,并對其過程進行抓包分析。效果如圖所示:
相關(guān)概念
●?Webview:提供 Web 控制能力,Web 組件提供網(wǎng)頁顯示能力。
●?HTTP數(shù)據(jù)請求:網(wǎng)絡(luò)管理模塊,提供 HTTP 數(shù)據(jù)請求能力,支持 GET、POST、OPTIONS、HEAD、PUT、DELETE、TRACE、CONNECT 請求方法。
●?HTTPS:應(yīng)用層協(xié)議,支持加密傳輸以及身份認(rèn)證,保證數(shù)據(jù)的安全傳輸。
●?SSL:SSL(Secure?Socket?Layer)安全套接層是位于傳輸通信協(xié)議(TCP/IP)之上實現(xiàn)的一種安全協(xié)議。
●?TLS:TLS(Transport?Layer?Security)是一種安全協(xié)議,旨在實現(xiàn)數(shù)據(jù)加密傳輸。
完整示例
gitee源碼地址
源碼下載
HTTPS請求過程(ArkTS).zip
環(huán)境搭建
我們首先需要完成 HarmonyOS 開發(fā)環(huán)境搭建,可參照如下步驟進行。
軟件要求
●?DevEco?Studio版本:DevEco?Studio?3.1?Release。?
●?HarmonyOS?SDK版本:API?version?9。
硬件要求
●?設(shè)備類型:華為手機或運行在 DevEco?Studio 上的華為手機設(shè)備模擬器。
●?HarmonyOS 系統(tǒng):3.1.0?Developer?Release。
環(huán)境搭建
1.? 安裝 DevEco?Studio,詳情請參考下載和安裝軟件。
2.? 設(shè)置 DevEco?Studio 開發(fā)環(huán)境,DevEco?Studio 開發(fā)環(huán)境需要依賴于網(wǎng)絡(luò)環(huán)境,需要連接上網(wǎng)絡(luò)才能確保工具的正常使用,可以根據(jù)如下兩種情況來配置開發(fā)環(huán)境:
●?如果可以直接訪問 Internet,只需進行下載HarmonyOS?SDK操作。
●?如果網(wǎng)絡(luò)不能直接訪問 Internet,需要通過代理服務(wù)器才可以訪問,請參考配置開發(fā)環(huán)境。
3.? 開發(fā)者可以參考以下鏈接,完成設(shè)備調(diào)試的相關(guān)配置:
●?使用真機進行調(diào)試
●?使用模擬器進行調(diào)試
代碼結(jié)構(gòu)解讀
本篇 Codelab 只對核心代碼進行講解,對于完整代碼,我們會在源碼下載或 gitee 中提供。
├──entry/src/main/ets????????????????//?代碼區(qū)
│??├──common
│??│??├──constants
│??│??│??├──StyleConstants.ets???????//?樣式常量類?
│??│??│??└──CommonConstants.ets??????//?常量類
│??│??└──utils
│??│?????├──HttpUtil.ets?????????????//?網(wǎng)絡(luò)請求方法
│??│?????└──Logger.ets???????????????//?日志打印工具類
│??├──entryability
│??│??└──EntryAbility.ts?????????????//?程序入口類
│??└──pages
│?????└──WebPage.ets?????????????????//?頁面入口
└──entry/src/main/resources??????????//?資源文件目錄
創(chuàng)建 HTTPS 請求
HTTPS 協(xié)議是位于應(yīng)用層的一種安全傳輸協(xié)議,與 HTTP 最大的區(qū)別是服務(wù)端與客戶端之間進行數(shù)據(jù)傳輸都會經(jīng)過 TLS/SSL 加密。該示例請求HarmonyOS官網(wǎng),并將請求得到的內(nèi)容通過 Web 容器展示出來。效果如圖所示:
首先在 HttpUtil.ets 中調(diào)用 createHttp 方法創(chuàng)建一個請求任務(wù),再通過 request 方法發(fā)起網(wǎng)絡(luò)請求。該方法支持三個參數(shù):url、options 以及 callback 回調(diào),其中 options 可以設(shè)置請求方法、請求頭以及超時時間等。
// 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;
}
接著在入口頁面中調(diào)用上述封裝的 httpGet 方法請求指定網(wǎng)址,將請求得到的內(nèi)容嵌入到 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 方法建立請求后,模塊底層首先會調(diào)用三方庫libcurl中的 curl_easy_init 初始化一個簡單會話。初始化完成后,接著調(diào)用 curl_easy_setopt 方法設(shè)置傳輸選項。其中 CURLOPT_URL 用于設(shè)置請求的 URL 地址,對應(yīng) request 中的 url 參數(shù);CURLOPT_WRITEFUNCTION 可以設(shè)置一個回調(diào),保存接收的數(shù)據(jù);CURLOPT_HEADERDATA 支持設(shè)置回調(diào),在回調(diào)中保存響應(yīng)頭數(shù)據(jù)。
//?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);
}
//?設(shè)置請求URL
NETSTACK_CURL_EASY_SET_OPTION(curl,?CURLOPT_URL,?context->options.GetUrl().c_str(),?context);
...
//?設(shè)置CURLOPT_WRITEFUNCTION傳輸選項,OnWritingMemoryBody為回調(diào)函數(shù)
NETSTACK_CURL_EASY_SET_OPTION(curl,?CURLOPT_WRITEFUNCTION,?OnWritingMemoryBody,?context);
NETSTACK_CURL_EASY_SET_OPTION(curl,?CURLOPT_WRITEDATA,?context,?context);
//?在OnWritingMemoryHeader寫入響應(yīng)頭數(shù)據(jù)
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;????????????????????????????????????????????????????????????????????????????????\
}????????????????????????????????????????????????????????????????????????????????????????????????\
傳輸選項設(shè)置成功后,調(diào)用 curl_multi_perform 執(zhí)行傳輸請求,并通過 curl_multi_info_read 查詢處理句柄是否有消息返回,最后進入 HandleCurlData 方法處理返回數(shù)據(jù)。
//?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 函數(shù)中調(diào)用 ParseHeaders 函數(shù)將上面回調(diào)寫入的響應(yīng)頭解析出來,其中響應(yīng)頭中會攜帶客戶端和服務(wù)端支持的最高網(wǎng)絡(luò)協(xié)議,如果是 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 中的網(wǎng)址協(xié)議頭更改為 http 時,在 DevEco?Studio 的日志中看到服務(wù)端會返回 301 狀態(tài)碼永久重定向到 https,因此最終通信依舊會經(jīng)歷 TLS 加密傳輸。
模塊源碼可以在 Gitee 開源倉庫 communication_netstack 中獲取,本篇 Codelab 引用源碼部分位于 http_exec 文件中。
TLS/SSL 握手過程
本章節(jié)主要通過抓包數(shù)據(jù)分析 TLS 協(xié)議的握手過程,其中包括交換參數(shù)、證書驗證、密鑰計算以及驗證密鑰等,抓包內(nèi)容如圖所示:
握手過程如圖所示:
5.1?第一次握手
根據(jù)上圖中可以看到,客戶端首先會進行第一次握手連接,發(fā)送“Client?Hello”消息給服務(wù)端開啟一個新的會話連接。分析數(shù)據(jù)包得到,客戶端在第一次握手時會向服務(wù)端傳遞協(xié)議版本號(TLS1.2)、隨機數(shù)(Client?Random,用于后續(xù)生成“會話密鑰”)、Session?ID 以及 Cipher?Suites(客戶端支持的密碼套件)。數(shù)據(jù)內(nèi)容如圖所示:
5.2?第二次握手
服務(wù)端接收到客戶端數(shù)據(jù)后,將響應(yīng)數(shù)據(jù)通過“Sever?Hello”傳遞給客戶端,包括隨機數(shù)(Sever?Random,用于后續(xù)生成“會話密鑰”)、協(xié)議版本號(TLS1.2)以及 Cipher?Suite(任意選擇一個客戶端支持的密碼套件),數(shù)據(jù)內(nèi)容如圖所示:
服務(wù)端傳遞“Sever?Hello”后,緊跟著會將 Certificate(證書)、“Sever?Key?Exchange”消息以及“Server?Hello?Done”消息傳遞給客戶端。此處著重分析“Sever?Key?Exchange”,數(shù)據(jù)內(nèi)容如圖所示:
5.3?第三次握手
客戶端收到“Server?Hello?Done”消息后,會將 Client?Params 數(shù)據(jù)傳遞給服務(wù)端,其中包含自身生成的橢圓曲線公鑰(Pubkey),數(shù)據(jù)內(nèi)容如圖所示:
經(jīng)過上述過程,客戶端持有 Client?Random、Server?Random 以及 Server?Params,將 Server?Params 使用服務(wù)端公鑰解密后得到“Server?Key?Exchange”消息中的臨時公鑰,客戶端使用 x25519 算法計算出預(yù)主密鑰(Premaster?Secret),然后再結(jié)合客戶端隨機數(shù)、服務(wù)端隨機數(shù)以及預(yù)主密鑰生成主密鑰,最終構(gòu)建“會話密鑰”?!癈hange?Cipher?Spec”消息表示客戶端已經(jīng)生成密鑰,并切換到加密模式。最后將之前所有的握手?jǐn)?shù)據(jù)做一個摘要,再利用雙方協(xié)商好的對稱密鑰進行加密,?通過“Encrypted?Handshake?Message”消息將加密數(shù)據(jù)傳遞給服務(wù)端做校驗。數(shù)據(jù)內(nèi)容如圖所示:
5.4?第四次握手
服務(wù)端利用 Client?Random、Server?Random 以及 Client?Params 計算得出“會話密鑰”,向客戶端傳遞“Change?Cipher?Spec”和“Encrypted?Handshake?Message”消息供客戶端校驗。當(dāng)雙方校驗通過后,真正的數(shù)據(jù)才開始傳輸。
總結(jié)
您已經(jīng)完成了本次 Codelab 的學(xué)習(xí),并了解到以下知識點:
1.? 使用 @ohos.net.http 建立一次 https 請求。文章來源:http://www.zghlxwxcb.cn/news/detail-772856.html
2.? 通過分析 TLS/SSL 握手過程中的傳輸數(shù)據(jù)包來理解數(shù)據(jù)安全傳輸。文章來源地址http://www.zghlxwxcb.cn/news/detail-772856.html
到了這里,關(guān)于基于 HarmonyOS 的 HTTPS 請求過程開發(fā)示例(ArkTS)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!