原因
首先 WebKit 進程是獨立于 app 進程之外的,兩個進程之間使用消息隊列的方式進行進程間通信。比如 app 想使用 WKWebView 加載一個請求,就要把請求的參數打包成一個 Message,然后通過 IPC 把 Message 交給 WebKit 去加載,反過來 WebKit 的請求想傳到 app 進程的話(比如 URLProtocol ),也要打包成 Message 走 IPC。出于性能的原因,打包的時候 HTTPBody 和 HTTPBodyStream 這兩個字段被丟棄掉了,這個可以參考 WebKit 的源碼,這就導致 -[WKWebView loadRequest:] 傳出的 HTTPBody 和 NSURLProtocol 傳回的 HTTPBody 全都被丟棄掉了。所以如果通過 NSURLProtocol 注冊攔截 http scheme,那么由 WebKit 發起的所有 http POST 請求就全都無效了,這個從原理上就是無解的。同時攔截后對 ATS 支持不好。
驗證過程
通過注冊NSURLProtocol并注冊私有API后進行NSURLRequest攔截,可以獲取 H5 發送的請求頭,但無法獲取 H5 端的請求。
1.WKWebView 攔截如圖:
2.UIWebView 攔截如圖:
解決方案
修改Scheme
將 H5 的資源文件與 POST 請求的鏈接使用不同的 Scheme ,移動端只攔截資源文件的 Scheme ,不攔截 POST 地址。
攔截方式:iOS 11 以上可使用 WKURLSchemeHandler 進行攔截,且只允許攔截自定義 Scheme 的請求,不允許攔截“http”、“https”、“ftp”、“file”等請求,否則會 crash。在 iOS 11 以下只能使用私有API:WKBrowsingContextController 和 registerSchemeForCustomProtocol ,通過反射的方式拿到了私有的 class/selector。POST 請求改為與原生交互
2.1 將 H5 對 POST 的交互改為與 Native 的橋接,由 Native 負責請求接口數據,再將數據返回給 JS。
2.2 注入一段 HookAjax 的 JS 代碼,攔截所有的 XMLHttpRequest 的 POST 請求轉移給移動端處理。將 POST 請求通過 JS 和 Native 交互的方式將請求轉交給 Native 處理并且在 Native 處理完后將結果返回給 JS。
小結:
方案1,移動端修改小,前端需要對數據所在的站點重新部署;
方案2.1,移動端、前端修改均較大;
方案2.2,移動端較大、前端修改較小,但需要有人幫忙寫 HookAjax 的 JS 代碼。
解決方法
上述的方案1、2.1對于前端改動較大,為了避免牽扯過多人員導致項目進展緩慢,則本文采用方案2.2。
1.注冊與注銷攔截
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
Class cls = NSClassFromString(@"IMYWebURLProtocol");
[NSURLProtocol registerClass:cls];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
Class cls = NSClassFromString(@"IMYWebURLProtocol");
[NSURLProtocol unregisterClass:cls];
}
2.設置與卸載WKWebViewConfiguration的hookAjax
WKUserContentController *wkUController = [[WKUserContentController alloc] init];
WKWebViewConfiguration *wkWebConfig = [[WKWebViewConfiguration alloc] init];
wkWebConfig.userContentController = wkUController;
[wkWebConfig.userContentController imy_installHookAjax]; // hookAjax
//卸載hookAjax
[wkConfig.userContentController imy_uninstallHookAjax];
至此,hookAjax已經結束,H5的post在被我們攔截后也能正常請求到數據了。代碼中涉及到的部分代碼來源于IMYWebLoader。不過經測試,如果H5加入eruda框架那么會導致沖突。于是筆者經過修改后編寫了一份新的js文件:github:WKHookAjax里的ajaxhook.js
2020.03.23更新
對Get請求方式也進行了Hook,因為iOS9下的Get方式請求體也為空。
參考資料
iOS - NSProtocol 攔截 WKWebView POST 請求 body 會被清空的問題解決
Web的一系列優化方案
Ajax-hook 原理解析
WKWebView 那些坑
iOS app秒開H5優化探索