WKWebView的Cookie问题小记

18,024 阅读9分钟

往者不可谏,来者犹可追

原文链接

一、Cookie和Session概述

Cookie和Session都是为了保存客户端和服务端之间的交互状态,实现机制不同,各有优缺点。

1、Cookie
  • Cookie是客户端请求服务端时,服务器会将一些信息以键值对的形式返回给客户端,保存在浏览器中,后续交互的时候可以带上这些Cookie值。用Cookie就可以方便的做一些缓存。
  • Cookie的缺点是大小和数量都有限制;Cookie是存在客户端的可能被禁用、删除、篡改,是不安全的;Cookie如果很大,每次要请求都要带上,这样就影响了传输效率。
  • Cookie的内容主要包括:名字过期时间路径路径一起构成Cookie的作用范围。若不设置过期时间,则表示这个Cookie的生命期为浏览器会话期间,关闭浏览器窗口,Cookie就消失。这种生命期为浏览器会话期的Cookie被称为会话Cookie会话Cookie一般不存储在硬盘上而是保存在内存里。
  • 若设置了过期时间,浏览器就会把Cookie保存到硬盘上,关闭后再次打开浏览器,这些Cookie仍然有效直到超过设定的过期时间。存储在硬盘上的Cookie可以在不同的浏览器进程间共享,比如两个IE窗口。而对于保存在内存里的Cookie,不同的浏览器有不同的处理方式 。
2、Session
  • Session是基于Cookie来实现的,不同的是Session本身存在于服务端,但是每次传输的时候不会将数据传输给客户端,只是把代表一个客户端的sessionid(jsessionid只是Tomcat中对sessionid的叫法)写在客户端的Cookie中,这样每次传输这个ID就可以了。

  • Session的优势就是传输数据量小,比较安全。Session有缺点,就是如果Session不做特殊的处理容易失效、过期、丢失或者Session过多导致服务器内存溢出,并且要实现一个稳定可用安全的分布式Session框架也是有一定复杂度的。在实际使用中就要结合Cookie和Session的优缺点针对不同的问题来设计解决方案。

3、理解
  • Session是一种服务器端的机制,服务器使用一种类似于散列表的结构(也可能就是使用散列表)来保存信息。
  • 当Server程序要为某个客户端的请求创建一个session时,服务器首先检查这个客户端的请求里是否已包含了一个session id,如果已包含则说明以前已经为此客户端创建过session,服务器就按照session id把这个session检索出来使用(检索不到,会新建一个);如果客户端请求不包含session id,则为此客户端创建一个session并且生成一个与此session相关联的session id,session id的值应该是一个既不会重复,又不容易被找到规律以仿造的字符串,这个session id将被在本次响应中返回给客户端保存。
  • 保存这个session id的方式可以采用Cookie,这样在交互过程中浏览器可以自动的按照规则把这个标识发送给服务器。一般这个cookie的名字都是类似于SEEESIONID。但Cookie可以被人为的禁止,则必须有其他机制以便在cookie被禁止时仍然能够把session id传递回服务器
  • 经常被使用的一种技术叫做URL重写,就是把session id直接附加在URL路径的后面。还有一种技术叫做表单隐藏字段。就是服务器会自动修改表单,添加一个隐藏字段,以便在表单提交时能够把session id传递回服务器。

二、WKWebView和Cookie

1、起因
WKWebView 发起的请求不会自动带上存储于 NSHTTPCookieStorage 容器中的 Cookie
  • 目前许多 H5 业务都依赖于 Cookie 作登录态校验,如果登陆是在 WebView 里做的,不会有什么问题;但是在很多场景下,在Native做登录,需要将登录信息带给WebView;但是在Native做了登录,也获取了Cookie信息,也使用 NSHTTPCookieStorage 将Cookie存到了本地;但是WKWebView在打开时候,不会自动去NSHTTPCookieStorage获取Cookie信息,这就是著名的首次 WKWebView 请求不携带 Cookie 的问题

  • WKWebView 实例其实会将 Cookie 存储于 NSHTTPCookieStorage 中,但存储时机有延迟,在iOS 8上,当页面跳转的时候,当前页面的 Cookie 会写入 NSHTTPCookieStorage 中,而在 iOS 10 上,JS 执行 document.cookie 或服务器 set-cookie 注入的 Cookie 会很快同步到 NSHTTPCookieStorage 中。

  • 其实,iOS11 可以解决首次 WKWebView 请求不携带 Cookie 的问题只要是存在 WKHTTPCookieStore 里的 cookie,WKWebView 每次请求都会携带

2、获取Cookie
// 方法一
    NSHTTPURLResponse *response = (NSHTTPURLResponse *)navigationResponse.response;
    NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:[response allHeaderFields] forURL:response.URL];
    NSLog(@"response-cookies = %@",cookies);
    
    //方法二
    NSString *cookieString = [[response allHeaderFields] valueForKey:@"Set-Cookie"];
    NSLog(@"cookieString = %@",cookieString);
    
    //方法三(如果有的话)
    NSArray<NSHTTPCookie *> *httpCookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
    NSLog(@"httpCookies = %@",httpCookies);
    
    //方法四
    if(@available(iOS 11, *)){
        //WKHTTPCookieStore的使用
        WKHTTPCookieStore *cookieStore = self.wkWebView.configuration.websiteDataStore.httpCookieStore;
        //获取 cookies
        [cookieStore getAllCookies:^(NSArray<NSHTTPCookie *> * _Nonnull cookies) {
            [cookies enumerateObjectsUsingBlock:^(NSHTTPCookie * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
                NSLog(@"cookieStore-cookies_%@:%@",@(idx),obj);
            }];
        }];
    }
    //将cookie设置到本地
    for (NSHTTPCookie *cookie in cookies) {
        //NSHTTPCookie cookie
        [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
    }
    
    decisionHandler(WKNavigationResponsePolicyAllow);
}

查看WKHTTPCookieStore中的某个cookie信息,如下

version:1
name:Hm_lvt_0c0e9d9b1e7d617b3e6842e85b9fb068
value:1554970993,1554971029,1554971246,1554971319
expiresDate:'2020-04-10 08:28:38 +0000'
created:'2019-04-11 08:28:38 +0000'
sessionOnly:FALSE
domain:.jianshu.com
partition:none
sameSite:none
path:/
isSecure:FALSE
path:"/" 
isSecure:FALSE
3、未过期Cookie持久化
  • 未过期的 Cookie被持久化存储在 NSLibraryDirectory 目录下的 Cookies/文件夹。

  • Cookie 持久化文件地址在 iOS 9+ 上在NSLibraryDirectory/Cookies,但是在 iOS 8 上 cookie 被保存在两部分,一部分如上所述,还有一部分保存在 App 无法获取的地方,/Users/Mac/Library/Developer/CoreSimulator/Devices/D2F74420-D59B-4A15-A50B-774D3D01FADE/data/Library/Cookies,大概就是后者的 Cookie 是 iOS 的 Safari 使用 。

  • 在 Cookies 目录下两个文件比较重要;

    Cookies.binarycookies
    <appid>.binarycookies
    

    两者的区别是 .binarycookies 是 NSHTTPCookieStorage 文件对象;.binarycookies 对应 WKWebview 的实例化对象。

三、WKWebView的Cookie注入

1、Javascript注入Cookie

在初始化 WKWebView 的时候,通过 WKUserScript 设置,使用Javascript 注入 Cookie

//js注入
WKUserContentController* userContentController = [[WKUserContentController alloc]init]; 
WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource: @"document.cookie ='CookieKey=CookieValue';"injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];  
[userContentController addUserScript:cookieScript]; 

WKWebViewConfiguration* webViewConfig = [[WKWebViewConfiguration alloc]init]; 
webViewConfig.userContentController = userContentController; 
WKWebView *webView = [[WKWebView alloc] initWithFrame:frame configuration:webViewConfig];
  • 通过 document.cookie 设置 Cookie (JS注入)解决后续页面(同域)Ajax、iframe 请求的 Cookie 问题;但是会遇到跨域丢失的问题,
  • 无法解决302请求的Cookie问题,假设第一个请求是 www.a.com,我们通过在 request header 里带上 Cookie 解决该请求的 Cookie 问题,接着页面302跳转到 www.b.com,这个时候 www.b.com 这个请求就可能因为没有携带 cookie 而无法访问。
  • 每一次页面跳转前都会调用回调函数decidePolicyForNavigationAction, 在这里拦截302请求,copy request,在 request header 中带上 cookie 并重新 loadRequest。
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
    //
}
  • 但是这种方法依然解决不了页面 iframe 跨域请求的 Cookie 问题,毕竟-[WKWebView loadRequest:]只适合加载 mainFrame 请求。
2、 NSMutableURLRequest 请求带上 Cookie
//request携带
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]]; 
//[request setHTTPShouldHandleCookies:YES];
[request setValue:[NSString stringWithFormat:@"%@=%@",@"CookieKey", @"CookieValue"] forHTTPHeaderField:@"Cookie"]; 
[webView loadRequest:request];       

说明:WKWebView loadRequest 前,在 request header 中设置 Cookie,可以解决(首个)请求 Cookie 带不上的问题;

3、WKHTTPCookieStore (iOS 11 later)
  • 利用iOS11 API WKHTTPCookieStore 解决 WKWebView 首次请求不携带 Cookie 的问题;这是因为:WKWebView每次请求都会携带 WKHTTPCookieStore 里的 Cookie。(WKWebView 使用 NSURLProtocol 拦截请求无法获取 Cookie 信息)
  • 在执行 [WKWebView loadRequest:] 前将 NSHTTPCookieStorage中的Cookie信息复制到 WKHTTPCookieStore 中,以此来达到 WKWebView中注入Cookie 的目的。示例代码如下:
if(@available(iOS 11, *)){
        //发送请求前插入cookie
    NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
    WKHTTPCookieStore *cookieStore = self.wkWebView.configuration.websiteDataStore.httpCookieStore;
    for (NSHTTPCookie *cookie in cookies) {
        [cookieStore setCookie:cookie completionHandler:^{
            //
        }];
    }
    [self.wkWebView loadRequest:request];
}
4、多WKWebView实例共享Cookie
  • Session 级别的 cookie 是保存在 WKProcessPool 里的,每个 WKWebview 都可以关联一个 WKProcessPool 的实例,如果需要在整个 App 生命周期里访问 h5 保留 h5 里的登录状态的,可以将使用 WKProcessPool 的单例来共享登录状态。

  • 让所有 WKWebView 共享同一个 WKProcessPool 实例,可以实现多个 WKWebView 之间共享 Cookie(session Cookie and persistent Cookie) 数据。不过 WKWebView WKProcessPool 实例在 App 杀进程重启后会被重置,导致 WKProcessPool 中的 Cookiesession Cookie 数据丢失,目前也无法实现 WKProcessPool 实例本地化保存。

//WKProcessPool+SharedProcessPool.h
@interface WKProcessPool (SharedProcessPool)

+ (WKProcessPool*)sharedProcessPool;

@end

//WKProcessPool+SharedProcessPool.m
@implementation WKProcessPool (SharedProcessPool)

+ (WKProcessPool*)sharedProcessPool {
    static WKProcessPool* shared;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        shared = [[WKProcessPool alloc] init];
    });
    return shared;
}
@end
    
//use
config.processPool = [WKProcessPool sharedProcessPool];    
self.wkWebView = [[WKWebView alloc]initWithFrame:self.view.bounds configuration:config];
[self.view addSubview:self.wkWebView];    
5、其他
  • H5地址是非Https,遇到奇怪的Cookie丢失问题。原因未知。

四、WKWebView中Cookie的清除

1、按内容删除
if (@available(iOS 9, *)){
        // 以www.baidu.com为例,是否包含baidu.com
        NSString *displayName = @"baidu.com";
        WKWebsiteDataStore *dataStore = [WKWebsiteDataStore defaultDataStore];
        [dataStore fetchDataRecordsOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] completionHandler:^(NSArray<WKWebsiteDataRecord *> * __nonnull records) {
            for (WKWebsiteDataRecord *record  in records){
                if ([displayName containsString:record.displayName]){
                    [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:record.dataTypes forDataRecords:@[record] completionHandler:^{
                        NSLog(@"Cookies for %@ deleted successfully",record.displayName);
                    }];
                }
            }
        }];
    }
2、按时间删除
- (void)removeWebViewDataCache:(NSDate *)sinceDate {
    
    if (@available(iOS 11.0, *)) {
        // iOS 9 以后终于可以使用 WKWebsiteDataStore 来清理缓存
        NSSet *websiteDataTypes = [WKWebsiteDataStore allWebsiteDataTypes];
        [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:websiteDataTypes modifiedSince:sinceDate completionHandler:^{
            NSLog(@"clear webView cache");
        }];
    } else {
        // iOS 8 可以通过清理 Library 目录下的 Cookies 目录来清除缓存
        NSString *libraryPath = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES).firstObject;
        NSString *cookiesFolderPath = [libraryPath stringByAppendingString:@"/Cookies"];
        [[NSFileManager defaultManager] removeItemAtPath:cookiesFolderPath error:nil];
    }
}

五、IP 直连方案对Cookie的影响

1、 存在的问题
  • 采用 IP 直连方案后,服务端返回的 Cookie 里的 Domain 字段也会使用 IP 。如果 IP 是动态的,就有可能导致一些问题:由于许多 H5 业务都依赖于 Cookie 作登录态校验,而 WKWebView 上请求不会自动携带 Cookie。
2、解决问题办法

六、推荐参考