iOS WKWebView 与 UIWebView Cookie
WKWebView概述
-
WKWebView是苹果在WWDC 2014 上推出的新一代WebView组件,相比iOS8及以前的UIWebView拥有更明显的优势
- 更多的支持HTML5的特性
- 高达60fps的滚动刷新率以及内置手势
- 将UIWebViewDelegate与UIWebView拆分成了14类和3个协议
- Safari相同的JS引擎
- 占用更少的内存
首先简单熟悉一下WKWebView的属性方法
// webview 配置
@property (nonatomic, readonly, copy) WKWebViewConfiguration *configuration;
//配置初始化方法
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
//WKPreferences偏好设置
config.preferences = [[WKPreferences alloc] init];
// 默认为0
config.preferences.minimumFontSize = 10;
// 默认认为YES
config.preferences.javaScriptEnabled = YES;
// 在iOS上默认为NO,表示不能自动通过窗口打开
config.preferences.javaScriptCanOpenWindowsAutomatically = NO;
// 导航代理
@property (nullable, nonatomic, weak) id <WKNavigationDelegate>navigationDelegate;
// 用户交互代理
@property (nullable, nonatomic, weak) id <WKUIDelegate> UIDelegate;
// 与UIWebView一样的加载请求API
- (nullable WKNavigation *)loadRequest:(NSURLRequest *)request;
// 直接加载HTML
- (nullable WKNavigation *)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL;
// 直接加载data
- (nullable WKNavigation *)loadData:(NSData *)data MIMEType:(NSString*)MIMEType characterEncodingName:(NSString *)characterEncodingName baseURL:(NSURL *)baseURL NS_AVAILABLE(10_11, 9_0);
// 停止加载数据
- (void)stopLoading;
// 执行JS代码
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ __nullable)(__nullable id, NSError * __nullable error))completionHandler;
- JS和WebView内容交互
// 只读属性,所有添加的WKUserScript都在这里可以获取到
@property (nonatomic, readonly, copy) NSArray<WKUserScript *> *userScripts;
// 注入JS
- (void)addUserScript:(WKUserScript *)userScript;
// 移除所有注入的JS
- (void)removeAllUserScripts;
// 添加scriptMessageHandler到所有的frames中,则都可以通过
// window.webkit.messageHandlers.<name>.postMessage(<messageBody>)
// 发送消息
// JS要调用原生的方法的方式
- (void)addScriptMessageHandler:(id<WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;
// 根据name移除所注入的scriptMessageHandler
- (void)removeScriptMessageHandlerForName:(NSString *)name;
- WKUserScript
在WKUserContentController中,所有使用到WKUserScript。WKUserContentController是用于与JS交互的类,而所注入的JS是WKUserScript对象。它的所有属性和方法如下:
// JS源代码
@property (nonatomic, readonly, copy) NSString *source;
// JS注入时间
@property (nonatomic, readonly) WKUserScriptInjectionTime injectionTime;
// 只读属性,表示JS是否应该注入到所有的frames中还是只有main frame.
@property (nonatomic, readonly, getter=isForMainFrameOnly) BOOLforMainFrameOnly;
// 初始化方法,用于创建WKUserScript对象
// source:JS源代码
// injectionTime:JS注入的时间
// forMainFrameOnly:是否只注入main frame
- (instancetype)initWithSource:(NSString *)source injectionTime:(WKUserScriptInjectionTime)injectionTime forMainFrameOnly:(BOOL)forMainFrameOnly;
- WKNavigationDelegate
@protocol WKNavigationDelegate <NSObject>
@optional
// 决定导航的动作,通常用于处理跨域的链接能否导航。WebKit对跨域进行了安全检查限制,不允许跨域,因此我们要对不能跨域的链接
// 单独处理。但是,对于Safari是允许跨域的,不用这么处理。
// 这个是决定是否Request
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
// 决定是否接收响应
// 这个是决定是否接收response
// 要获取response,通过WKNavigationResponse对象获取
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler;
// 当main frame的导航开始请求时,会调用此方法
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation;
// 当main frame接收到服务重定向时,会回调此方法
- (void)webView:(WKWebView *)webViewdidReceiveServerRedirectForProvisionalNavigation:(null_unspecified WKNavigation*)navigation;
// 当main frame开始加载数据失败时,会回调
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;
// 当main frame的web内容开始到达时,会回调
- (void)webView:(WKWebView *)webView didCommitNavigation:(null_unspecified WKNavigation *)navigation;
// 当main frame导航完成时,会回调
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation;
// 当main frame最后下载数据失败时,会回调
- (void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;
// 这与用于授权验证的API,与AFN、UIWebView的授权验证API是一样的
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *__nullable credential))completionHandler;
// 当web content处理完成时,会回调
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView NS_AVAILABLE(10_11, 9_0);
@end
typedef NS_ENUM(NSInteger, WKNavigationActionPolicy) {
WKNavigationActionPolicyCancel,
WKNavigationActionPolicyAllow,
} NS_ENUM_AVAILABLE(10_10, 8_0);
WKWebView 和 UIWebView 的 Cookie 同步问题
- 由于 WKWebView 是 iOS9 之后推出的 webview 组件,在这之前都是 UIWebView 。所以为了不影响 iOS8 上的 UIWebView 的使用,就要对 WKWebView 和 UIWebView 做封装兼容处理。其中 Cookie 的同步问题是其中的一个大坑。
WKWebView 的 Cookie 机制:
-
WKWebView 的 Cookie 存储在它的私有存储 WKWebsiteDataStore 中。 WKWebsiteDataStore 中存储了包括 cookies、disk、memory caches、WebSQL、IndexedDB 数据库和本地存储等Web内容。
-
为什么说 WKWebsiteDataStore 是私有存储容器呢 打开 WKWebsiteDataStore 的头文件我们可以看到:
//defaultDataStore 是默认选择的存储容器
+ (WKWebsiteDataStore *)defaultDataStore;
//nonPersistentDataStore会禁止任何数据写入文件系统,可用于无痕浏览
+ (WKWebsiteDataStore *)nonPersistentDataStore;
//可以查看到容器中存储的网站数据的所有种类
+ (NSSet<NSString *> *)allWebsiteDataTypes;
//以下分别是获取容器中的数据记录和删除数据记录的方法
- (void)fetchDataRecordsOfTypes:(NSSet<NSString *> *)dataTypes completionHandler:(void (^)(NSArray<WKWebsiteDataRecord *> *))completionHandler;
- (void)removeDataOfTypes:(NSSet<NSString *> *)dataTypes forDataRecords:(NSArray<WKWebsiteDataRecord *> *)dataRecords completionHandler:(void (^)(void))completionHandler;
- (void)removeDataOfTypes:(NSSet<NSString *> *)websiteDataTypes modifiedSince:(NSDate *)date completionHandler:(void (^)(void))completionHandler;
WKWebsiteDataStore 中的Web数据是以 WKWebsiteDataRecord 类的形式保存的,我们可以通过 fetchDataRecordsOfTypes 方法获取到 datastore 中的 record 数据。但是当我们打开 WKWebsiteDataRecord 类的头文件的时候就会发现:
@interface WKWebsiteDataRecord : NSObject
/*! @abstract The display name for the data record. This is usually the domain name. */
@property (nonatomic, readonly, copy) NSString *displayName;
/*! @abstract The various types of website data that exist for this data record. */
@property (nonatomic, readonly, copy) NSSet<NSString *> *dataTypes;
@end
- 它一共就两个公有属性:displayName 和 dataTypes,分别是该 record 数据的域名名称 和 该 record 中保存了哪些类型web数据。我们可以动手看看这里面到底是什么东西。
WKWebsiteDataStore *dataStore = [WKWebsiteDataStore defaultDataStore];
[dataStore fetchDataRecordsOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] completionHandler:^(NSArray<WKWebsiteDataRecord *> * _Nonnull records) {
for (WKWebsiteDataRecord *record in records)
{
NSLog(@"++++++++++++++++%@",[record description]);
}
}];
- 结果输出如下:
- <WKWebsiteDataRecord: 0x1700e8a00; displayName = meitu.com; dataTypes = { Memory Cache, Cookies }>
- 所以在 WKWebsiteDataRecord 中我们是拿不到任何cookie数据的。
UIWebView 的 Cookie 机制
UIWebView 在浏览网页后会将网页中的 cookie 自动存入 NSHTTPCookieStorage 标准容器中。在后续访问中会将 cookie 自动带到 request 请求当中。比如,NSHTTPCookieStorage 中存储了一个Cookie,name=a;value=b;domain=y.qq.com;expires=Sat,02 May 2017 23:20:25 GMT; 则通 过 UIWebView 发起请求 http://y.qq.com,则请求头会自动带上cookie,而通过 WKWebView 发起请求,请求头不会自动带上该cookie。
初步解决方案
其实主要要做的只有两步,1、获取Cookie,2、注入Cookie
1、获取Cookie
-
由于 WKWebView 的 Cookie 存储容器 WKWebsiteDataStore 是私有存储,所以无法从这里获取到Cookie,目前的方法是(1)从网站返回的 response headerfields 中获取。(2)通过调用js的方法获取 cookie。
-
(1)从网站返回的 response headerfields 中获取
-
因为cookie都存在http respone的headerfields,找到能获得respone的WKWebView回调,打印
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler{
NSHTTPURLResponse *response = (NSHTTPURLResponse *)navigationResponse.response;
NSArray *cookies =[NSHTTPCookie cookiesWithResponseHeaderFields:[response allHeaderFields] forURL:response.URL];
//读取wkwebview中的cookie 方法1
for (NSHTTPCookie *cookie in cookies) {
// [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
NSLog(@"wkwebview中的cookie:%@", cookie);
}
//读取wkwebview中的cookie 方法2 读取Set-Cookie字段
NSString *cookieString = [[response allHeaderFields] valueForKey:@"Set-Cookie"];
NSLog(@"wkwebview中的cookie:%@", cookieString);
//看看存入到了NSHTTPCookieStorage了没有
NSHTTPCookieStorage *cookieJar2 = [NSHTTPCookieStorage sharedHTTPCookieStorage];
for (NSHTTPCookie *cookie in cookieJar2.cookies) {
NSLog(@"NSHTTPCookieStorage中的cookie%@", cookie);
}
decisionHandler(WKNavigationResponsePolicyAllow);
}
- (2)通过调用js的方法获取 cookie。
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation
{
[webView evaluateJavaScript:[NSString stringWithFormat:@"document.cookie"] completionHandler:^(id _Nullable response, NSError * _Nullable error) {
if (response != 0) {
NSLog(@"\n\n\n\n\n\n document.cookie%@,%@",response,error);
}
}];
}
-
获取 Cookie 的过程中碰到的坑
-
1、通过(2)中 document.cookie 的方法获取 cookie 有一定的可行性,但是实践之后发现获取到的cookie 并不全面,该方法无法获取到 httponly 的cookie。如果用 https://www.zhihu.com 试验一下就会发现,其中 z_c0 的 cookie 是无法被获取到的,因为它是 httponly 属性的cookie。
-
2、不论是(1)还是(2)方法,似乎都无法解决302请求的 Cookie 问题。举例来说,假设你要访问网站A,在A中点击登录,跳转页面到B地址,在B中完成登录之后302跳转回A网站。此时cookie是存在于B地址的 response 中的,在A地址的 response 中并没有 cookie 的字段。然而我们只能获取到A地址的 response ,无法截获到B地址的response。因此获取不到该类型网站的 cookie 。(该问题还在尝试解决中,如果有解决方案的小伙伴希望能告知我一声感激不尽)
2、注入Cookie
-
注入 Cookie 就是从之前保存 cookie 的 NSHTTPCookieStorage 中取出相关 Cookie,然后在再次请求访问的时候在 request 中注入 Cookie。注入Cookie同样有多种方式。
-
(1)JS注入1
//取出 storage 中的cookie并将其拼接成正确的形式
NSArray<NSHTTPCookie *> *tmp = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
NSMutableString *jscode_Cookie = [@"" mutableCopy];
[tmp enumerateObjectsUsingBlock:^(NSHTTPCookie * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSLog(@"%@ = %@", obj.name, obj.value);
[jscode_Cookie appendString:[NSString stringWithFormat:@"document.cookie = '%@=%@';", obj.name, obj.value]];
}];
WKUserContentController* userContentController = WKUserContentController.new;
WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource: @"" injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
[userContentController addUserScript:cookieScript];
WKWebViewConfiguration* webViewConfig = WKWebViewConfiguration.new;
webViewConfig.userContentController = userContentController;
WKWebView * webView = [[WKWebView alloc] initWithFrame:CGRectMake(/*set your values*/) configuration:webViewConfig];
- (2)JS注入2
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
[webView evaluateJavaScript:@"document.cookie ='TeskCookieKey1=TeskCookieValue1';" completionHandler:^(id result, NSError *error) {
//...
}];
}
- (3) NSMutableURLRequest 注入Cookie
NSURL *url = request.URL;
NSMutableString *cookies = [NSMutableString string];
NSMutableURLRequest *requestObj = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:10];
NSArray *tmp = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
NSDictionary *dicCookies = [NSHTTPCookie requestHeaderFieldsWithCookies:tmp];
NSString *cookie = [self readCurrentCookie];
[requestObj setValue:cookie forHTTPHeaderField:@"Cookie"];
[_webView loadRequest:requestObj];
-(NSString *)readCurrentCookie{
NSMutableDictionary *cookieDic = [NSMutableDictionary dictionary];
NSMutableString *cookieValue = [NSMutableString stringWithFormat:@""];
NSHTTPCookieStorage *cookieJar = [NSHTTPCookieStorage sharedHTTPCookieStorage];
for (NSHTTPCookie *cookie in [cookieJar cookies]) {
[cookieDic setObject:cookie.value forKey:cookie.name];
}
// cookie重复,先放到字典进行去重,再进行拼接
for (NSString *key in cookieDic) {
NSString *appendString = [NSString stringWithFormat:@"%@=%@;", key, [cookieDic valueForKey:key]];
[cookieValue appendString:appendString];
}
return cookieValue;
}
- 注入 Cookie 的过程中碰到的坑
(1)JS注入的Cookie,比如PHP代码在Cookie容器中取是取不到的,直接js document.cookie能读取到
NSMutableURLRequest 注入的PHP等动态语言直接能从$_COOKIE对象中获取到
(2)app退出后 cookie 丢失的问题
- 在开发过程中发现 cookie 的保存上出现了问题,在注入 cookie 的时候发现部分 Cookie 丢失。网上找到另外一种解决方案是说 cookie 过期,需要手动设置保存一下时间。默认下,cookies 的保存时间是 app 退出后 cookie 就会被清掉。可参考:http://www.jianshu.com/p/1e402922ee32/ 。虽然我在设置了 cookie 的有效期之后,还是存在 cookie 丢失的问题。(叹气
- 于是后来采用 NSUserDefault 在获取到 cookie 的时候保存 cookie,在重新访问的时候从 NSUserDefault 中取出 cookie 再设置一遍,从而得到之前保存的 cookie 并注入request 中
//修改保存 cookie 到storage的方法
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(nonnull WKNavigationResponse *)navigationResponse decisionHandler:(nonnull void (^)(WKNavigationResponsePolicy))decisionHandler{
NSHTTPURLResponse *response = (NSHTTPURLResponse *)navigationResponse.response;
NSArray *cookies =[NSHTTPCookie cookiesWithResponseHeaderFields:[response allHeaderFields] forURL:response.URL];
NSString *cookieString = [[response allHeaderFields] valueForKey:@"Set-Cookie"];
for (NSHTTPCookie *cookie in cookies) {
[[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
}
NSMutableArray *cookieArray = [[NSMutableArray alloc] init];
for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]) {
[cookieArray addObject:cookie.name];
NSMutableDictionary *cookieProperties = [NSMutableDictionary dictionary];
[cookieProperties setObject:cookie.name forKey:NSHTTPCookieName];
[cookieProperties setObject:cookie.value forKey:NSHTTPCookieValue];
[cookieProperties setObject:cookie.domain forKey:NSHTTPCookieDomain];
[cookieProperties setObject:cookie.path forKey:NSHTTPCookiePath];
[cookieProperties setObject:[NSNumber numberWithInt:cookie.version] forKey:NSHTTPCookieVersion];
[cookieProperties setObject:[[NSDate date] dateByAddingTimeInterval:2629743] forKey:NSHTTPCookieExpires];
[[NSUserDefaults standardUserDefaults] setValue:cookieProperties forKey:cookie.name];
[[NSUserDefaults standardUserDefaults] synchronize];
}
[[NSUserDefaults standardUserDefaults] setValue:cookieArray forKey:@"cookieArray"];
[[NSUserDefaults standardUserDefaults] synchronize];
//清除WKWebView的Cookie,之后删除
WKWebsiteDataStore *dataStore = [WKWebsiteDataStore defaultDataStore];
[dataStore fetchDataRecordsOfTypes:[WKWebsiteDataStore allWebsiteDataTypes]
completionHandler:^(NSArray<WKWebsiteDataRecord *> * __nonnull records) {
for (WKWebsiteDataRecord *record in records)
{
// if ( [record.displayName containsString:@"baidu"]) //取消备注,可以针对某域名清除,否则是全清
// {
[[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:record.dataTypes
forDataRecords:@[record]
completionHandler:^{
NSLog(@"Cookies for %@ deleted successfully",record.displayName);
}];
// }
}
}];
decisionHandler(WKNavigationResponsePolicyAllow);
}
//修改从 storage 中读取 cookie 的方法
-(NSString *)readCurrentCookie{
NSMutableArray* cookieDictionary = [[NSUserDefaults standardUserDefaults] valueForKey:@"cookieArray"];
NSLog(@"cookie dictionary found is %@",cookieDictionary);
for (int i=0; i < cookieDictionary.count; i++)
{
NSLog(@"cookie found is %@",[cookieDictionary objectAtIndex:i]);
NSMutableDictionary* cookieDictionary1 = [[NSUserDefaults standardUserDefaults] valueForKey:[cookieDictionary objectAtIndex:i]];
NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:cookieDictionary1];
[[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
}
NSMutableDictionary *cookieDic = [NSMutableDictionary dictionary];
NSMutableString *cookieValue = [NSMutableString stringWithFormat:@""];
NSHTTPCookieStorage *cookieJar = [NSHTTPCookieStorage sharedHTTPCookieStorage];
for (NSHTTPCookie *cookie in [cookieJar cookies]) {
[cookieDic setObject:cookie.value forKey:cookie.name];
}
// cookie重复,先放到字典进行去重,再进行拼接
for (NSString *key in cookieDic) {
// if ([key isEqualToString:@"nweb_qa"] || [key isEqualToString:@"z_c0"] || [key isEqualToString:@"_xsrf"]) {
NSString *appendString = [NSString stringWithFormat:@"%@=%@;", key, [cookieDic valueForKey:key]];
[cookieValue appendString:appendString];
// }
}
return cookieValue;
}
结语
- WKWebView cookie 的同步问题中的坑还是挺多的,而且目前还是没有解决获取重定向地址Cookie的问题,有相关建议或者困惑的同学可以评论或者私聊一起讨论解决一下。