【iOS】WKWebView使用Cookies遇到的坑

Apple推出WKWebView已经有一段时间了,相对于UIWebView而言,内存占用只有UIWebView的一半左右,但是响应速度和效率上却是UIWebView的两倍。
  总结WKWebView使用方法的帖子文章很多,这里不再赘述,这里重点总结一下cookies共享问题。

WKWebView会忽视默认的网络存储, NSURLCache, NSHTTPCookieStorage, NSCredentialStorage。 目前是这样的,WKWebView有自己的进程,同样也有自己的存储空间用来存储cookie和cache, 其他的网络类如NSURLConnection是无法访问到的。 同时WKWebView发起的资源请求也是不经过NSURLProtocol的,导致无法自定义请求。

由于以上原因,导致WKWebView无法与App自身的Cookies、UIWebView之间共享Cookies数据。WKWebView这样做也有一定的好处,在不用操作原有Cookies的基础上,独立的一套Cookies,有效的防止了Web与App Api接口的Cookie相互污染。
  但是在使用过程中也遇到了一些坑。

有的时候因为业务需求,就是需要相互共享Cookies,这样的案例也有很多,支付宝中大量的Web中,应该就有很多Cookie是和App共享的吧。


NSHTTPCookieStorage Cookies共享

在WkWebView接收到Response后,将Response带的Cookies取出,然后直接放入[NSHTTPCookieStorage sharedHTTPCookieStorage] 容器中:

- (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];
    for (NSHTTPCookie *cookie in cookies) {
        [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
    }
    decisionHandler(WKNavigationResponsePolicyAllow);
}

然后在完全加载完成后:

// 页面加载完成之后调用
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation{
    //取出cookie
    NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
    //js函数
    NSString *JSFuncString =
    @"function setCookie(name,value,expires)\
    {\
    var oDate=new Date();\
    oDate.setDate(oDate.getDate()+expires);\
    document.cookie=name+'='+value+';expires='+oDate+';path=/'\
    }\
    function getCookie(name)\
    {\
    var arr = document.cookie.match(new RegExp('(^| )'+name+'=({FNXX==XXFN}*)(;|$)'));\
    if(arr != null) return unescape(arr[2]); return null;\
    }\
    function delCookie(name)\
    {\
    var exp = new Date();\
    exp.setTime(exp.getTime() - 1);\
    var cval=getCookie(name);\
    if(cval!=null) document.cookie= name + '='+cval+';expires='+exp.toGMTString();\
    }";

    //拼凑js字符串
    NSMutableString *JSCookieString = JSFuncString.mutableCopy;
    for (NSHTTPCookie *cookie in cookieStorage.cookies) {
        NSString *excuteJSString = [NSString stringWithFormat:@"setCookie('%@', '%@', 1);", cookie.name, cookie.value];
        [JSCookieString appendString:excuteJSString];
    }
    //执行js
    [webView evaluateJavaScript:JSCookieString completionHandler:^(id obj, NSError * _Nullable error) {
        NSLog(@"%@",error);
    }];
}

为什么在完全加载完成后需要重新给WKWebView设置Cookie呢?如果你不这样做的话很有可能因为a标签跳转,导致下一次跳转的时候Cookie丢失。

302跳转Set-Cookie丢失

上面的方法可以将服务器Set-Cookie携带到下一次请求中。但是如果302跳转出现在你的第一次加载并且你使用了下面的方法设置第一次加载的Cookies,那么在302跳转时服务器Set-Cookie将不会被携带到下一次302跳转的目标请求中。

NSMutableURLRequest *request= [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https:/a.com/test1"]];
[request setValue:[NSString stringWithFormat:@"%@=%@",@"a", @"1"] forHTTPHeaderField:@"Cookie"];

比如:第一次加载https:/a.com/test1,然后加载https:/a.com/test1设置Cookie为a=1,服务器在https:/a.com/test1中Set-Cookie a=2;然后302跳转到https:/a.com/test2,这个时候会发现https:/a.com/test2中获取到的a还是1,Set-Cookie并没有成功。

解决办法

1.加载一个本地为空的html,域名指向你的第一次加载的url的域名。

//加载本地html
[self.webView loadHTMLString:@"" baseURL:[NSURL URLWithString:@"https:/a.com"]];

2.通过以下方法,在第一次加载完成后,将需要设置的Cookies设置到WKWebView中,因为是加载的本地的html以下方法会立即执行。

// 页面加载完成之后调用
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation{
    
    if (isFirstLoaded) {
        NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
        //js函数
        NSString *JSFuncString =
        @"function setCookie(name,value,expires)\
        {\
        var oDate=new Date();\
        oDate.setDate(oDate.getDate()+expires);\
        document.cookie=name+'='+value+';expires='+oDate+';path=/'\
        }";
        
        //拼凑js字符串,按照自己的需求拼凑Cookie
        NSMutableString *JSCookieString = JSFuncString.mutableCopy;
        for (NSHTTPCookie *cookie in cookieStorage.cookies) {
            if (![cookie.name isEqualToString:@"__cust"]) {
                NSString *excuteJSString = [NSString stringWithFormat:@"setCookie('%@', '%@', 3);", cookie.name, cookie.value];
                [JSCookieString appendString:excuteJSString];
            }
        }
        
        //执行js
        [webView evaluateJavaScript:JSCookieString completionHandler:^(id obj, NSError * _Nullable error) {
            //加载真正的第一次Request
            [self loadRealRequest];
        }];
    }
}

WKWebView中Cookie混乱

按道理来说每个WKWebView都有一个单独的存储Cookies的空间,相互不影响,但是,奇妙之处就是我在一个UIViewController中生成了一个WKWebView,然后进行了一系列的网络访问后,推出并销毁这个UIViewcontroller;在下次进来的时候这个WKWebView会携带上次访问的部分Cookies。
  这个原因是WKWebView会将Cookie存储到沙盒目录的文件中,下次WKWebView被实例化的时候,会去同步这个文件中的Cookies,如果不希望它去同步之歌Cookies,那么直接删掉好了。

- (void)deleteWebCache {
    if ([[UIDevice currentDevice].systemVersion floatValue] >= 9.0) {
        NSSet *websiteDataTypes = [NSSet setWithArray:@[
                                WKWebsiteDataTypeDiskCache,
                                //WKWebsiteDataTypeOfflineWebApplicationCache,
                                WKWebsiteDataTypeMemoryCache,
                                //WKWebsiteDataTypeLocalStorage,
                                //WKWebsiteDataTypeCookies,
                                //WKWebsiteDataTypeSessionStorage,
                                //WKWebsiteDataTypeIndexedDBDatabases,
                                //WKWebsiteDataTypeWebSQLDatabases
                                ]];
        //// All kinds of data
        //NSSet *websiteDataTypes = [WKWebsiteDataStore allWebsiteDataTypes];
        //// Date from
        NSDate *dateFrom = [NSDate dateWithTimeIntervalSince1970:0];
        //// Execute
        [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:websiteDataTypes modifiedSince:dateFrom completionHandler:^{
            // Done
        }];
    } else {
        NSString *libraryPath = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0];
        NSString *cookiesFolderPath = [libraryPath stringByAppendingString:@"/Cookies"];
        NSError *errors;
        [[NSFileManager defaultManager] removeItemAtPath:cookiesFolderPath error:&errors];
    }

}

不足之处,欢迎轻轻地喷一下......

推荐阅读更多精彩内容