iOS WKWebView 与 UIWebView Cookie

作者: 沙琪玛dd | 来源:发表于2017-03-19 17:23 被阅读3549次

    WKWebView概述

    • WKWebView是苹果在WWDC 2014 上推出的新一代WebView组件,相比iOS8及以前的UIWebView拥有更明显的优势

      1. 更多的支持HTML5的特性
      2. 高达60fps的滚动刷新率以及内置手势
      3. 将UIWebViewDelegate与UIWebView拆分成了14类和3个协议
      4. Safari相同的JS引擎
      5. 占用更少的内存

    首先简单熟悉一下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的问题,有相关建议或者困惑的同学可以评论或者私聊一起讨论解决一下。

    相关文章

      网友评论

      • 爱情公寓:楼主 在吗 我最近在做这方面的。想请教你 我三个步骤都设置了 还是注入不了cookie
      • 爱情公寓:有的话 可以给我个demo吗 万分感谢 。。过不好年了。。最后关头 整出这个来 。。
      • 爱情公寓:请问大神们 现在都有好的方案没 啊
      • child_cool:大佬,方便私聊一下嘛,我想问问关于wkwebview的同步问题
        qq562925462
      • 咖啡豆8888:谢谢楼主分享
      • sea_waves:请问大神有 demo吗?可以把你的demo的链接发一下吗?万分感谢啦
      • zyg:关于 cookie 登录相关还是走本地登录吧 在必要的地方通过js 注入
        这样问题少很多~
      • bnmlkj:遇到了 302请求的 Cookie 问题, 请问 lz 这部分有更新吗?
      • 440bd488f596::disappointed_relieved: 好多坑啊,找了一天都没看到一篇好的解决方案
        dapeng199:亲,找到好的解决方案了吗,求推荐啊
        _哼哼_:wk iOS8就有了
        沙琪玛dd:@440bd488f596 加油哦

      本文标题:iOS WKWebView 与 UIWebView Cookie

      本文链接:https://www.haomeiwen.com/subject/kdfvnttx.html