以下内容转载http://xiaozhuanlan.com/topic/2517460839 终端杂谈的一篇文章
前言
如果开发者一开始选择 UIWebView 作为 h5 的容器,开发者无须关注 cookie 的存取问题,因为后台下发的cookie会自动存储到NSHTTPCookieStorage这个容器中,webView内部的请求会自动从NSHTTPCookieStorage获取合适的cookie带上去。
但是苹果爸爸说UIWebView在iOS12之后是一个deprecated接口,希望开发者迁移到WKWebView中。但是WKWebView关于cookie的存取却不像UIWebView那么方便,主要是因为整个框架的改变,WKWebView已经不单单是在app这一侧,WKWebView容器发起的h5请求已不在app进程中发起和响应处理,而是在专门的web进程中处理,所以WKWebView的网络请求无法直接从NSHTTPCookieStorage取到cookie,所以当h5访问一些带鉴权的接口就会出现问题。
下面我提供个case给大家解析cookie机制在新容器是如何运转和提供正确的设置姿势。
案例
首先我们先在我们的代码打个log,将后台下发的cookie给打印出来,也验证下后台是否下发成功了:
通过控制台log,发现后台下发的cookie已经同步到NSHTTPCookieStorage,我们打印出cookie的key和value:
接着我启动我们的WKWebView容器发起第一个请求:
同时我们启动Charles进行网络抓包,抓取webview中的网络请求:
图中那个请求就是webview容器的第一个请求,它会进行鉴权相关的判断,通过查看这个请求的请求体发现请求体里没有cookie字段,说明这个请求没有带上cookie。对应的业务展示也变成系统异常:
接下开启Safari的开发调试模式,调试当前这个h5界面,我们发现后台下发的cookie已经同步到WKWebView侧,这是因为WKWebView内也有cookie的容器,而且每隔一段时间就和app侧NSHTTPCookieStorage进行同步,而且这个同步是进程级别的同步,而且这个同步是单向,这个后面会进行解析。为什么第一次请求没有带上cookie是因为app侧NSHTTPCookieStorage的cookie还没同步给WKWebView的cookieStorage,导致网络请求没有带上cookie,当我们开启Safari的开发调试模式的时候已经完成同步,所以我们可以看到对应WKWebView的cookieStorage存的cookie跟app控制台log的cookie一样:
如果这时候我们点击Safari调试器的刷新按钮,响应会是业务正常,因为刷新重新发起请求,这时候请求已经带上cookie
设置cookie的正确姿势
在iOS11之后,苹果爸爸终于理解开发者关于设置cookie的痛楚,所以开放一个接口给我们设置WKWebView的cookieStorage,要注意这个接口是异步的,所以我们需要等待WKWebView异步设置cookie完成后才发起请求:
@wb_weakify(self)
if(@available(iOS11.0, *)) {
[self.webview.configuration.websiteDataStore.httpCookieStoresetCookie:eventCKcompletionHandler:^{
@wb_strongify(self)
NSURLRequest *request = [NSURLRequestrequestWithURL:url];
[self.webviewloadRequest:request];
}];
为了适配iOS11以前的系统版本,设置cookie就没有那么直接,我们查阅网上很多文章,甚至是一些比较权威的团队发出来的文章,都是以下这种思路:通过key-Value构造一个cookie,WKWebView loadRequest 前,在 request header 中设置 Cookie, 解决首个请求 Cookie 带不上的问题,通过 document.cookie 设置 Cookie 解决后续页面(同域)Ajax、iframe 请求的 Cookie 问题:
NSMutableURLRequest*request = [NSMutableURLRequestrequestWithURL:url];
[request setValue:[NSStringstringWithFormat:@"%@;",[self_getCookieString:eventCK]] forHTTPHeaderField:@"Cookie"];
WKUserScript* cookieScript = [[WKUserScriptalloc] initWithSource:
[NSStringstringWithFormat:@"document.cookie = '%@';",[self_getCookieString:eventCK]]
injectionTime:WKUserScriptInjectionTimeAtDocumentStartforMainFrameOnly:NO];
[_webview.configuration.userContentController addUserScript: cookieScript];
[_webview loadRequest:request];
- (NSString*)_getCookieString:(NSHTTPCookie*)cookie {
NSString*string = [NSStringstringWithFormat:@"%@=%@;",
cookie.name,
cookie.value];
returnstring;
}
按着这些文章改造我们的代码还是没有解决业务鉴权失败的问题,因为这些文章都忽律一个关键的操作,就是cookie的构建,很多文章只是简单构造key-Value,导致因为同源策略请求带不上cookie。cookie有四个关键的标识:value(键值对)、expires(过期日期)、domain(域)、path(路径),如果有一个标识不一样,它就是一个新的cookie,在iOS中如果cookie只指定value,其他会设置为默认值,我们通过Safari调试器捕获这个cookie:
我们可以看到WKWebView的cookieStorage存在两个同名同值的cookie,但是他们的域、路径、过期时间都不同,我们可以看出只设置key-Value的cookie默认的域就是发起的请求的url的域名,对应的路径也是发起请求的url的path,第二个cookie是正确的但他是后面同步过来的,第一个cookie也就是我们通过key-Value构建的cookie,请求带不上去这个cookie,因为它与业务请求不同源(业务请求只支持域为".webank.com"和path为"/"的cookie)。所以正确的姿势应该是构造一个全cookie:
- (NSString*)_getCookieString:(NSHTTPCookie*)cookie {
NSString*string = [NSStringstringWithFormat:@"%@=%@;domain=%@;expiresDate=%@;path=%@;sessionOnly=%@;isSecure=%@",
cookie.name,
cookie.value,
cookie.domain,
cookie.expiresDate,
cookie.path ?:@"/",
cookie.isSecure ?@"TRUE":@"FALSE",
cookie.sessionOnly ?@"TRUE":@"FALSE"];
returnstring;
}
经过这样设置之后,业务请求终于能够带上正确的cookie:
CookieStorage同步是单向的?
想必很多基于WKWebView开发的同学都看过bugly的这篇文章《WKWebView 那些坑》,里面提到Cookie那一块内容讲到“ WKWebView 实例其实也会将 Cookie 存储于 NSHTTPCookieStorage 中,但存储时机有延迟,在iOS 8上,当页面跳转的时候,当前页面的 Cookie 会写入 NSHTTPCookieStorage 中,而在 iOS 10 上,JS 执行 document.cookie 或服务器 set-cookie 注入的 Cookie 会很快同步到 NSHTTPCookieStorage 中”。其实这种说法讲的不够准确,只有最后一话的后半句是正确,正确的说法是cookie是同步是单向,且只有app侧的NSHTTPCookieStorage将cookie同步到WKWebView维护的cookieStorage,WKWebView的cookie无法同步到NSHTTPCookieStorage中,本着对读者负责的态度,我进行实验进行验证,验证代码如下,验证步骤已经注释在代码中:
//WKWebView请求带上cookie
WKUserScript* cookieScript = [[WKUserScriptalloc] initWithSource:
[NSStringstringWithFormat:@"document.cookie = '%@';",[self_getCookieString:eventCK]]
injectionTime:WKUserScriptInjectionTimeAtDocumentStartforMainFrameOnly:NO];
[_webview.configuration.userContentController addUserScript: cookieScript];
[_webview loadRequest:request];
//一般cookie同步应该在很短时间内完成,我们假定一个较长的时间10s,10s后从NSHTTPCookieStorage获取所有cookie,验证WKWebView的cookie有没有同步到这里
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10*NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSArray*cookies = [NSHTTPCookieStoragesharedHTTPCookieStorage].cookies;
for(NSHTTPCookie*ckincookies) {
NSLog(@"app cookies : [%@:%@]",ck.name,ck.value);
}
});
//生成测试cookie,key为testWKCK,为了让请求带上cookie,cookie的domain和path设置为符合业务要求的值
- (NSString*)_getCookieString:(NSHTTPCookie*)cookie {
NSString*string = [NSStringstringWithFormat:@"%@=%@;domain=%@;expiresDate=%@;path=%@;sessionOnly=%@;isSecure=%@",
@"testWKCK",
@"111111111",
cookie.domain,
cookie.expiresDate,
cookie.path ?:@"/",
cookie.isSecure ?@"TRUE":@"FALSE",
cookie.sessionOnly ?@"TRUE":@"FALSE"];
returnstring;
}
结果验证:
通过Sarafi调试器我们可以看到我们WKWebView存在一个名为testWKCK的cookie,我们弄个10秒的定时器,10秒后打印app侧所有cookie,发现只有后台下发的那个cookie,名为testWKCK的cookie在app侧没有保存,这说明了cookie的同步是单向,只有从app侧到webThread侧。
既然是单向同步。那怎么保证WKWebView的cookie也同步到App的NSHTTPCookieStorage?在iOS11之后,苹果提供一个接口用于配置observer监听cookie的改变:
WK_EXTERNAPI_AVAILABLE(macosx(10.13), ios(11.0))
@interfaceWKHTTPCookieStore:NSObject
/*! @abstract Adds a WKHTTPCookieStoreObserver object with the cookie store.
@param observer The observer object to add.
@discussion The observer is not retained by the receiver. It is your responsibility
to unregister the observer before it becomes invalid.
*/
- (void)addObserver:(id)observer;
/*! @abstract Removes a WKHTTPCookieStoreObserver object from the cookie store.
@param observer The observer to remove.
*/
- (void)removeObserver:(id)observer;
@end
如果为了适配iOS11之前的版本,则需要native这边注册一个同步cookie的JS函数,用于h5将新的cookie同步到native这边。
cookie不会自动覆盖?
cookie会同名覆盖(保证上文说的四要素相同),但有时候后台可能下发的cookie某个要素不一致导致cookie不会自动覆盖,或者存在测试、生产域切换缓存,为了保险起见你可以每次重写cookie之前要先删除旧cookie:
//清理cookie
NSArray*oldCookies = [[NSHTTPCookieStoragesharedHTTPCookieStorage].cookiescopy];
for(NSHTTPCookie*ckinoldCookies) {
if([ck.name isEqualToString:@"capToken"]) {
[[NSHTTPCookieStoragesharedHTTPCookieStorage] deleteCookie:ck];
}
}