iGBlog

iGuan7u

iOS 出身的小伙子,对 Objective-C、Swift、以及 Javascript 有浓厚的兴趣。热衷使用原生实现所有功能,厌恶一切的跨平台开发技术。喜欢分享工作过程中遇到的问题以及日常工作中遇到的新技术。希望这个博客能给你带来一点启发。

GET IN TOUCH

期待与你的联系。

WKWebView 设置 Cookie 的正确方式

1. 前言:

WKWebView 万般好,Cookie 操作对比 UIWebView,仿佛就是处于原始时代一般。

笔者在开发过程中遇到了一个问题,在 Server 端返回的 302 响应中,响应头部的 Set-Cookie 字段全部没有生效。深究下去后,掀开了 WKWebView 英俊的外表下,及其丑恶的嘴脸。

2. 问题

笔者的场景中,在使用 WKWebView 打开的页面,Server 端需要校验 Client 的身份。因此需要在打开网页前,注入当前用户的身份信息。由于 Cookie 支持 Request 的其中一个头部字段,因此可以很简单的操作为:

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request addValue:cookieValue forHTTPHeaderField:@"Cookie"];
[webView loadRequest:request];

然后,他就生效了,Server 能正确获取到 Cookie 数据,问题得以解决。

直到,其后的需求变更,其中一种场景下,Request 会触发一个 HTTP 状态码为 302 的跨域跳转,同时,Response 中会通过 Set-Cookie 字段注入目的地址所需的身份信息。

然后,跳转失败,302 后的 Request 并没有带上该有的 Cookie 数据。

3. 原因

究其根本,笔者认为该是 WKWebView 所处的是独立进程的原因。
众所周知,不同于 UIWebView,WKWebView 所在的进程是独立于 App 的进程的,因此 WKWebView 无法像 UIWebView 一样轻松获取应用的 NSHTTPCookieStorage 的单例对象,转而需要进程间通讯传递所需的参数。

这才是 WKWebView Cookie 管理的正确方式 文章中说到:

结合两者,你也会发现一个核心的概念-如果设置了 allHTTPHeaderFields,则不用使用 the cookie manager by default

通过 NSMutableURLRequest 修改 Header 字段,默认不再需要 WKWebView 自动管理 Cookie 信息,因此在笔者的场景中,302 后的 Cookie 由于没有自行管理,因此便被丢弃。

4. 解决方法

这才是 WKWebView Cookie 管理的正确方式 文章中提到,在设置 Cookie 的时候,应该避免通过 NSMutableURLRequest 设置 Cookie 信息。然后该作者提供了一种通过 Javascript 方式注入 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;

这种方式其实是不准确的,由于 JS 注入 Cookie 的时机是 WKUserScriptInjectionTimeAtDocumentStart ,这意味着需要等 WKWebView 发起了首次请求,获取到 HTML 数据的时候,Cookie 并没有注入。这种方式只适用于获取 HTML 的 Request 不校验 Cookie,只是其中的 Ajax 才需要 Cookie 信息的场景。

笔者认为,正确的应该是通过 iOS 11.0 版本引入的 WKHTTPCookieStore 成员去注入 Cookie 信息到 WKWebView 中

// 建议自行初始化 DataSource,使用 WKWebView 默认的有几率不生效
WKWebsiteDataStore *dataStore = [WKWebsiteDataStore nonPersistentDataStore];
WKWebViewConfiguration *wkWebConfig = [[WKWebViewConfiguration alloc] init];
wkWebConfig.websiteDataStore = dataStore;

WKWebView *webview = [[WKWebView alloc] initWithFrame:CGRectZero configuration:wkWebConfig];
// 获取 Cookie Store 对象
WKHTTPCookieStore *cookieStore = webView.configuration.websiteDataStore.httpCookieStore;

// 由于设置 Cookie 应该是跨进程通讯,这里需要等待设置完成后,再发起请求
dispatch_group_t cookieGroup = dispatch_group_create();
for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:request.URL]) {
dispatch_group_enter(cookieGroup);
[cookieStore setCookie:cookie completionHandler:^{
dispatch_group_leave(cookieGroup);
}];
}

dispatch_group_notify(cookieGroup, dispatch_get_main_queue(), ^{
[webView loadRequest:request];
});

另外对于从 WKWebView 同步 Cookie 到 NSHTTPCookieStorage 的场景,可以通过增加 WKHTTPCookieStore 的 Observer ,实现代理 <WKHTTPCookieStoreObserver> (cookiesDidChangeInCookieStore:) 事件,即可将变更的 Cookie 同步回 NSHTTPCookieStorage 中。