說起 WKWebview 代替 UIWebview 帶來的好處你可以舉出一堆堆的例子,但說到 WKWebview 的問題,你繞不過的就是 WKWebview cookie 和 NSHTTPCookieStorage cookie 不共享的問題。你可以在網絡上搜到如何將他們相互同步的帖子。
如何將 NSHTTPCookieStorage 同步給 WKWebview ,大概要處理很多種情況,包括但不限于以下;
- 初次加載頁面時,同步 cookie 到 WKWebview
- 處理 ajax 請求時,需要的 cookie
- 如果 response 里有 set-cookie 還需要緩存這些 cookie
- 如果是 302 還需要處理 cookie 傳遞的問題
所以,如果你按照上面的要求編寫了代碼,你會發現總有漏網之魚的情況沒有處理,比方說請求 response 設置了 cookie,為了在后續跳轉中帶上這些 cookie,你需要暫存下來,這樣可能會污染到 NSHTTPCookieStorage ;再舉一個極端的真實的案例,如果有個網站的鑒權是通過 302 鑒權 和 response set-cookie 的,那么你會發現這個網站在鑒權那里陷入了死循環,因為 302 response set-cookie 后 302 的 location 地址加載時并沒有攜帶上 302 時設置的 cookie,進而繼續 302 set-cookie的跳轉。
那如果解決 302 response set-cookie 的問題,我們不能在上述方案里修修補補,上述方案對正常的數據請求已經有很大的侵入性,對很多沒有必要進行 cookie 設置的頁面做了處理,一定程度上對性能也有影響。讓我們跳脫原來的方案,重新審視下 WKWebview cookie 相關的資料。
WKWebview cookie 是怎么存儲的
-
session 級別的 cookie
session 級別的 cookie 是保存在WKProcessPool
里的,每個 WKWebview 都可以關聯一個WKProcessPool
的實例,如果需要在整個 App 生命周期里訪問 h5 保留 h5 里的登錄狀態的,可以將使用WKProcessPool
的單例來共享登錄狀態。
WKProcessPool
是個沒有屬性和方法的對象,唯一的作用就是標識是不是需要新的 session 級別的管理對象,一個實例代表一個對象。
-
未過期的 cookie
有有效期的 cookie 被持久化存儲在NSLibraryDirectory
目錄下的Cookies/
文件夾。
image.png
注意,cookie 持久化文件地址在 iOS 9+ 上在
/Users/Mac/Library/Developer/CoreSimulator/Devices/D2F74420-D59B-4A15-A50B-774D3D01FADE/data/Containers/Data/Application/E8646AD5-1110-43F3-95D9-DE6A32E78DB7/Library/Cookies
.
但是在 iOS 8 上 cookie 被保存在兩部分,一部分如上所述,還有一部分保存在 App 無法獲取的地方,/Users/Mac/Library/Developer/CoreSimulator/Devices/D2F74420-D59B-4A15-A50B-774D3D01FADE/data/Library/Cookies
,大概就是后者的 Cookie 是 iOS 的 Safari 使用 。
在 Cookies 目錄下兩個文件比較重要;
- Cookie.binarycookies
- <appid>.binarycookies
兩者的區別是 <appid>.binarycookies 是 NSHTTPCookieStorage 文件對象;Cookie.binarycookies 則是 WKWebview 的實例化對象。
這也是為什么 WKWebview 和 NSHTTPCookieStorage 的原因——因為被保存在不同的文件當中。
為了驗證,你可以打開這兩者文件進行查看,這里不再展開。
當然兩個文件都是 binary file,直接用文本瀏覽器打開是看不到,有一個 python 寫的腳本
BinaryCookieReader
https://gist.github.com/sh1n0b1/4bb8b737370bfe5f5ab8。可以讀出來
WKWebview Cookie 是如何工作的?
- 當 webview loadRequest 或者 302 或者在 webview 加載完畢,觸發了 ajax 請求時,WKWebview 所需的 Cookie 會去 Cookie.binarycookies 里讀取本域名下的 Cookie ,加上
WKProcessPool
持有的 Cookie 一起作為 request 頭里的 Cookie 數據。 - 但是如果仔細查看
NSURLRequest.h
源代碼,而不是僅僅查看NSDictionary<NSString *, NSString *> *allHTTPHeaderFields;
的 quick help,你會發現這句話;
@abstract Sets the HTTP header fields of the receiver to the given
dictionary.
@discussion This method replaces all header fields that may have
existed before this method call.
再查看下HTTPShouldHandleCookies
的 quick help,
@property BOOL HTTPShouldHandleCookies;
Description
A boolean value that indicates whether the receiver should use the default cookie handling for the request.
YES if the receiver should use the default cookie handling for the request, NO otherwise. The default is YES.
If your app sets the Cookie header on an NSMutableURLRequest object, then this method has no effect, and the cookie data you set in the header overrides all cookies from the cookie store.
SDKs iOS 8.0+, macOS 10.10+, tvOS 9.0+, watchOS 2.0+
結合兩者,你也會發現一個核心的概念-如果設置了 allHTTPHeaderFields,則不用使用 the cookie manager by default。
所以我們的方案是-在頁面加載過程中不去設置 allHTTPHeaderFields,全部使用默認 Cookie mananger
管理,這樣就不會有 Cookie 污染也不會有 302 Cookie 丟失的問題了,下面讓我們驗證一下。
唯一的問題——如何將 NSHTTPCookieStorage 的 Cookie 共享給 WKWebview。
解決方案
在首次加載 url 時,檢查是否已經同步過 Cookie。如果沒有同步過,則先加載 一個 cookieWebivew,它的主要目的就是將 Cookie 先使用 usercontroller 的方式寫到 WKWebview 里,這樣在處理正式的請求時,就會帶上我們從 NSHTTPCookieStorage 獲取到的 Cookie了。
核心代碼如下,
if ([AppHostCookie loginCookieHasBeenSynced] == NO) {
//
NSURL *cookieURL = [NSURL URLWithString:kFakeCookieWebPageURLString];
NSMutableURLRequest *mutableRequest = [NSMutableURLRequest requestWithURL:cookieURL cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:120];
WKWebView *cookieWebview = [self getCookieWebview];
[self.view addSubview:cookieWebview];
[cookieWebview loadRequest:mutableRequest];
DDLogInfo(@"[JSBridge] preload cookie for url = %@", self.loadUrl);
} else {
[self loadWebPage];
}
// 注意,CookieWebview 和 正常的 webview 是不同的
- (WKWebView *)getCookieWebview
{
// 設置加載頁面完畢后,里面的后續請求,如 xhr 請求使用的cookie
WKUserContentController *userContentController = [WKUserContentController new];
WKWebViewConfiguration *webViewConfig = [[WKWebViewConfiguration alloc] init];
webViewConfig.userContentController = userContentController;
webViewConfig.processPool = [AppHostCookie sharedPoolManager];
NSMutableArray<NSString *> *oldCookies = [AppHostCookie cookieJavaScriptArray];
[oldCookies enumerateObjectsUsingBlock:^(NSString *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) {
NSString *setCookie = [NSString stringWithFormat:@"document.cookie='%@';", obj];
WKUserScript *cookieScript = [[WKUserScript alloc] initWithSource:setCookie injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES];
[userContentController addUserScript:cookieScript];
}];
WKWebView *webview = [[WKWebView alloc] initWithFrame:CGRectMake(0, -1, SCREEN_WIDTH,ONE_PIXEL) configuration:webViewConfig];
webview.navigationDelegate = self;
webview.UIDelegate = self;
return webview;
}
這里需要處理的問題是,加載完畢或者失敗后需要清理舊 webview 和設置標記位。
static NSString * _Nonnull kFakeCookieWebPageURLString = @"http://ai.api.com/xhr/user/getUid.do?26u-KQa-fKQ-3BD"
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
{
NSURL *targetURL = webView.URL;
if ([AppHostCookie loginCookieHasBeenSynced] == NO && targetURL.query.length > 0 && [kFakeCookieWebPageURLString containsString:targetURL.query]) {
[AppHostCookie setLoginCookieHasBeenSynced:YES];
// 加載真正的頁面;此時已經有 App 的 cookie 存在了。
[webView removeFromSuperview];
[self loadWebPage];
return;
}
}
同時記得刪掉原來對 webview 的 Cookie 的所有處理的代碼。
處理至此,大功告成,這樣的后續請求, WKWebview 都用自身所有的 Cookie 和 NSHTTPCookieStorage 的 Cookie,這樣既達到了 Cookie 共享的目的, WKWebview 和 NSHTTPCookieStorage 的 Cookie 也做了個隔離。