说起 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/
文件夹。
注意,
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
文件对象;<appid>.binarycookies
则是 WKWebview
的实例化对象。
这也是为什么 WKWebview
和 NSHTTPCookieStorage
的原因——因为被保存在不同的文件当中。
为了验证,你可以打开这两者文件进行查看,这里不再展开。
当然两个文件都是
binary file
,直接用文本浏览器打开是看不到,有一个python
写的脚本BinaryCookieReader
gist.github.com/sh1n0b1/4bb…。可以读出来
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
也做了个隔离。