这才是 WKWebview Cookie 管理的正确方式

说起 WKWebview 代替 UIWebview 带来的好处你可以举出一堆堆的例子,但说到 WKWebview 的问题,你绕不过的就是 WKWebview cookie 和 NSHTTPCookieStorage cookie 不共享的问题。你可以在网络上搜到如何将他们相互同步的帖子。

如何将 NSHTTPCookieStorage 同步给 WKWebview ,大概要处理很多种情况,包括但不限于以下;

  1. 初次加载页面时,同步 cookie 到 WKWebview
  2. 处理 ajax 请求时,需要的 cookie
  3. 如果 response 里有 set-cookie 还需要缓存这些 cookie
  4. 如果是 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 是怎么存储的

  1. session 级别的 cookie
    session 级别的 cookie 是保存在 WKProcessPool 里的,每个 WKWebview 都可以关联一个 WKProcessPool 的实例,如果需要在整个 App 生命周期里访问 h5 保留 h5 里的登录状态的,可以将使用 WKProcessPool 的单例来共享登录状态。

WKProcessPool 是个没有属性和方法的对象,唯一的作用就是标识是不是需要新的 session 级别的管理对象,一个实例代表一个对象。

  1. 未过期的 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 写的脚本 BinaryCookieReaderhttps://gist.github.com/sh1n0b1/4bb8b737370bfe5f5ab8。可以读出来

WKWebview Cookie 是如何工作的?

  1. 当 webview loadRequest 或者 302 或者在 webview 加载完毕,触发了 ajax 请求时,WKWebview 所需的 Cookie 会去 Cookie.binarycookies 里读取本域名下的 Cookie ,加上
    WKProcessPool持有的 Cookie 一起作为 request 头里的 Cookie 数据。
  2. 但是如果仔细查看 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 也做了个隔离。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容