WKWebView的Cookie問題小記

南華Coder發表於2019-04-20

往者不可諫,來者猶可追

原文連結

一、Cookie和Session概述

Cookie和Session都是為了儲存客戶端和服務端之間的互動狀態,實現機制不同,各有優缺點。

1、Cookie
  • Cookie是客戶端請求服務端時,伺服器會將一些資訊以鍵值對的形式返回給客戶端,儲存在瀏覽器中,後續互動的時候可以帶上這些Cookie值。用Cookie就可以方便的做一些快取。
  • Cookie的缺點是大小和數量都有限制;Cookie是存在客戶端的可能被禁用、刪除、篡改,是不安全的;Cookie如果很大,每次要請求都要帶上,這樣就影響了傳輸效率。
  • Cookie的內容主要包括:名字過期時間路徑路徑一起構成Cookie的作用範圍。若不設定過期時間,則表示這個Cookie的生命期為瀏覽器會話期間,關閉瀏覽器視窗,Cookie就消失。這種生命期為瀏覽器會話期的Cookie被稱為會話Cookie會話Cookie一般不儲存在硬碟上而是儲存在記憶體裡。
  • 若設定了過期時間,瀏覽器就會把Cookie儲存到硬碟上,關閉後再次開啟瀏覽器,這些Cookie仍然有效直到超過設定的過期時間。儲存在硬碟上的Cookie可以在不同的瀏覽器程式間共享,比如兩個IE視窗。而對於儲存在記憶體裡的Cookie,不同的瀏覽器有不同的處理方式 。
2、Session
  • Session是基於Cookie來實現的,不同的是Session本身存在於服務端,但是每次傳輸的時候不會將資料傳輸給客戶端,只是把代表一個客戶端的sessionid(jsessionid只是Tomcat中對sessionid的叫法)寫在客戶端的Cookie中,這樣每次傳輸這個ID就可以了。

  • Session的優勢就是傳輸資料量小,比較安全。Session有缺點,就是如果Session不做特殊的處理容易失效、過期、丟失或者Session過多導致伺服器記憶體溢位,並且要實現一個穩定可用安全的分散式Session框架也是有一定複雜度的。在實際使用中就要結合Cookie和Session的優缺點針對不同的問題來設計解決方案。

3、理解
  • Session是一種伺服器端的機制,伺服器使用一種類似於雜湊表的結構(也可能就是使用雜湊表)來儲存資訊。
  • 當Server程式要為某個客戶端的請求建立一個session時,伺服器首先檢查這個客戶端的請求裡是否已包含了一個session id,如果已包含則說明以前已經為此客戶端建立過session,伺服器就按照session id把這個session檢索出來使用(檢索不到,會新建一個);如果客戶端請求不包含session id,則為此客戶端建立一個session並且生成一個與此session相關聯的session id,session id的值應該是一個既不會重複,又不容易被找到規律以仿造的字串,這個session id將被在本次響應中返回給客戶端儲存。
  • 儲存這個session id的方式可以採用Cookie,這樣在互動過程中瀏覽器可以自動的按照規則把這個標識傳送給伺服器。一般這個cookie的名字都是類似於SEEESIONID。但Cookie可以被人為的禁止,則必須有其他機制以便在cookie被禁止時仍然能夠把session id傳遞迴伺服器
  • 經常被使用的一種技術叫做URL重寫,就是把session id直接附加在URL路徑的後面。還有一種技術叫做表單隱藏欄位。就是伺服器會自動修改表單,新增一個隱藏欄位,以便在表單提交時能夠把session id傳遞迴伺服器。

二、WKWebView和Cookie

1、起因
WKWebView 發起的請求不會自動帶上儲存於 NSHTTPCookieStorage 容器中的 Cookie
複製程式碼
  • 目前許多 H5 業務都依賴於 Cookie 作登入態校驗,如果登陸是在 WebView 裡做的,不會有什麼問題;但是在很多場景下,在Native做登入,需要將登入資訊帶給WebView;但是在Native做了登入,也獲取了Cookie資訊,也使用 NSHTTPCookieStorage 將Cookie存到了本地;但是WKWebView在開啟時候,不會自動去NSHTTPCookieStorage獲取Cookie資訊,這就是著名的首次 WKWebView 請求不攜帶 Cookie 的問題

  • WKWebView 例項其實會將 Cookie 儲存於 NSHTTPCookieStorage 中,但儲存時機有延遲,在iOS 8上,當頁面跳轉的時候,當前頁面的 Cookie 會寫入 NSHTTPCookieStorage 中,而在 iOS 10 上,JS 執行 document.cookie 或伺服器 set-cookie 注入的 Cookie 會很快同步到 NSHTTPCookieStorage 中。

  • 其實,iOS11 可以解決首次 WKWebView 請求不攜帶 Cookie 的問題只要是存在 WKHTTPCookieStore 裡的 cookie,WKWebView 每次請求都會攜帶

2、獲取Cookie
// 方法一
    NSHTTPURLResponse *response = (NSHTTPURLResponse *)navigationResponse.response;
    NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:[response allHeaderFields] forURL:response.URL];
    NSLog(@"response-cookies = %@",cookies);
    
    //方法二
    NSString *cookieString = [[response allHeaderFields] valueForKey:@"Set-Cookie"];
    NSLog(@"cookieString = %@",cookieString);
    
    //方法三(如果有的話)
    NSArray<NSHTTPCookie *> *httpCookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
    NSLog(@"httpCookies = %@",httpCookies);
    
    //方法四
    if(@available(iOS 11, *)){
        //WKHTTPCookieStore的使用
        WKHTTPCookieStore *cookieStore = self.wkWebView.configuration.websiteDataStore.httpCookieStore;
        //獲取 cookies
        [cookieStore getAllCookies:^(NSArray<NSHTTPCookie *> * _Nonnull cookies) {
            [cookies enumerateObjectsUsingBlock:^(NSHTTPCookie * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
                NSLog(@"cookieStore-cookies_%@:%@",@(idx),obj);
            }];
        }];
    }
    //將cookie設定到本地
    for (NSHTTPCookie *cookie in cookies) {
        //NSHTTPCookie cookie
        [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
    }
    
    decisionHandler(WKNavigationResponsePolicyAllow);
}
複製程式碼

檢視WKHTTPCookieStore中的某個cookie資訊,如下

version:1
name:Hm_lvt_0c0e9d9b1e7d617b3e6842e85b9fb068
value:1554970993,1554971029,1554971246,1554971319
expiresDate:'2020-04-10 08:28:38 +0000'
created:'2019-04-11 08:28:38 +0000'
sessionOnly:FALSE
domain:.jianshu.com
partition:none
sameSite:none
path:/
isSecure:FALSE
path:"/" 
isSecure:FALSE
複製程式碼
3、未過期Cookie持久化
  • 未過期的 Cookie被持久化儲存在 NSLibraryDirectory 目錄下的 Cookies/資料夾。

  • Cookie 持久化檔案地址在 iOS 9+ 上在NSLibraryDirectory/Cookies,但是在 iOS 8 上 cookie 被儲存在兩部分,一部分如上所述,還有一部分儲存在 App 無法獲取的地方,/Users/Mac/Library/Developer/CoreSimulator/Devices/D2F74420-D59B-4A15-A50B-774D3D01FADE/data/Library/Cookies,大概就是後者的 Cookie 是 iOS 的 Safari 使用 。

  • 在 Cookies 目錄下兩個檔案比較重要;

    Cookies.binarycookies
    <appid>.binarycookies
    複製程式碼

    兩者的區別是 .binarycookies 是 NSHTTPCookieStorage 檔案物件;.binarycookies 對應 WKWebview 的例項化物件。

三、WKWebView的Cookie注入

1、Javascript注入Cookie

在初始化 WKWebView 的時候,通過 WKUserScript 設定,使用Javascript 注入 Cookie

//js注入
WKUserContentController* userContentController = [[WKUserContentController alloc]init]; 
WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource: @"document.cookie ='CookieKey=CookieValue';"injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];  
[userContentController addUserScript:cookieScript]; 

WKWebViewConfiguration* webViewConfig = [[WKWebViewConfiguration alloc]init]; 
webViewConfig.userContentController = userContentController; 
WKWebView *webView = [[WKWebView alloc] initWithFrame:frame configuration:webViewConfig];
複製程式碼
  • 通過 document.cookie 設定 Cookie (JS注入)解決後續頁面(同域)Ajax、iframe 請求的 Cookie 問題;但是會遇到跨域丟失的問題,
  • 無法解決302請求的Cookie問題,假設第一個請求是 www.a.com,我們通過在 request header 裡帶上 Cookie 解決該請求的 Cookie 問題,接著頁面302跳轉到 www.b.com,這個時候 www.b.com 這個請求就可能因為沒有攜帶 cookie 而無法訪問。
  • 每一次頁面跳轉前都會呼叫回撥函式decidePolicyForNavigationAction, 在這裡攔截302請求,copy request,在 request header 中帶上 cookie 並重新 loadRequest。
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
    //
}
複製程式碼
  • 但是這種方法依然解決不了頁面 iframe 跨域請求的 Cookie 問題,畢竟-[WKWebView loadRequest:]只適合載入 mainFrame 請求。
2、 NSMutableURLRequest 請求帶上 Cookie
//request攜帶
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]]; 
//[request setHTTPShouldHandleCookies:YES];
[request setValue:[NSString stringWithFormat:@"%@=%@",@"CookieKey", @"CookieValue"] forHTTPHeaderField:@"Cookie"]; 
[webView loadRequest:request];       
複製程式碼

說明:WKWebView loadRequest 前,在 request header 中設定 Cookie,可以解決(首個)請求 Cookie 帶不上的問題;

3、WKHTTPCookieStore (iOS 11 later)
  • 利用iOS11 API WKHTTPCookieStore 解決 WKWebView 首次請求不攜帶 Cookie 的問題;這是因為:WKWebView每次請求都會攜帶 WKHTTPCookieStore 裡的 Cookie。(WKWebView 使用 NSURLProtocol 攔截請求無法獲取 Cookie 資訊)
  • 在執行 [WKWebView loadRequest:] 前將 NSHTTPCookieStorage中的Cookie資訊複製到 WKHTTPCookieStore 中,以此來達到 WKWebView中注入Cookie 的目的。示例程式碼如下:
if(@available(iOS 11, *)){
        //傳送請求前插入cookie
    NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
    WKHTTPCookieStore *cookieStore = self.wkWebView.configuration.websiteDataStore.httpCookieStore;
    for (NSHTTPCookie *cookie in cookies) {
        [cookieStore setCookie:cookie completionHandler:^{
            //
        }];
    }
    [self.wkWebView loadRequest:request];
}
複製程式碼
4、多WKWebView例項共享Cookie
  • Session 級別的 cookie 是儲存在 WKProcessPool 裡的,每個 WKWebview 都可以關聯一個 WKProcessPool 的例項,如果需要在整個 App 生命週期裡訪問 h5 保留 h5 裡的登入狀態的,可以將使用 WKProcessPool 的單例來共享登入狀態。

  • 讓所有 WKWebView 共享同一個 WKProcessPool 例項,可以實現多個 WKWebView 之間共享 Cookie(session Cookie and persistent Cookie) 資料。不過 WKWebView WKProcessPool 例項在 App 殺程式重啟後會被重置,導致 WKProcessPool 中的 Cookiesession Cookie 資料丟失,目前也無法實現 WKProcessPool 例項本地化儲存。

//WKProcessPool+SharedProcessPool.h
@interface WKProcessPool (SharedProcessPool)

+ (WKProcessPool*)sharedProcessPool;

@end

//WKProcessPool+SharedProcessPool.m
@implementation WKProcessPool (SharedProcessPool)

+ (WKProcessPool*)sharedProcessPool {
    static WKProcessPool* shared;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        shared = [[WKProcessPool alloc] init];
    });
    return shared;
}
@end
    
//use
config.processPool = [WKProcessPool sharedProcessPool];    
self.wkWebView = [[WKWebView alloc]initWithFrame:self.view.bounds configuration:config];
[self.view addSubview:self.wkWebView];    
複製程式碼
5、其他
  • H5地址是非Https,遇到奇怪的Cookie丟失問題。原因未知。

四、WKWebView中Cookie的清除

1、按內容刪除
if (@available(iOS 9, *)){
        // 以www.baidu.com為例,是否包含baidu.com
        NSString *displayName = @"baidu.com";
        WKWebsiteDataStore *dataStore = [WKWebsiteDataStore defaultDataStore];
        [dataStore fetchDataRecordsOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] completionHandler:^(NSArray<WKWebsiteDataRecord *> * __nonnull records) {
            for (WKWebsiteDataRecord *record  in records){
                if ([displayName containsString:record.displayName]){
                    [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:record.dataTypes forDataRecords:@[record] completionHandler:^{
                        NSLog(@"Cookies for %@ deleted successfully",record.displayName);
                    }];
                }
            }
        }];
    }
複製程式碼
2、按時間刪除
- (void)removeWebViewDataCache:(NSDate *)sinceDate {
    
    if (@available(iOS 11.0, *)) {
        // iOS 9 以後終於可以使用 WKWebsiteDataStore 來清理快取
        NSSet *websiteDataTypes = [WKWebsiteDataStore allWebsiteDataTypes];
        [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:websiteDataTypes modifiedSince:sinceDate completionHandler:^{
            NSLog(@"clear webView cache");
        }];
    } else {
        // iOS 8 可以通過清理 Library 目錄下的 Cookies 目錄來清除快取
        NSString *libraryPath = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES).firstObject;
        NSString *cookiesFolderPath = [libraryPath stringByAppendingString:@"/Cookies"];
        [[NSFileManager defaultManager] removeItemAtPath:cookiesFolderPath error:nil];
    }
}
複製程式碼

五、IP 直連方案對Cookie的影響

1、 存在的問題
  • 採用 IP 直連方案後,服務端返回的 Cookie 裡的 Domain 欄位也會使用 IP 。如果 IP 是動態的,就有可能導致一些問題:由於許多 H5 業務都依賴於 Cookie 作登入態校驗,而 WKWebView 上請求不會自動攜帶 Cookie。
2、解決問題辦法

六、推薦參考

相關文章