WKWebView 的 Cookies 同步笔记

3,231 阅读5分钟

这是我参与更文挑战的第7天,活动详情查看: 更文挑战

WKWebView 的 Cookies 同步

iOS Cookie 的管理

NSHTTPCookie和NSHTTPCookieStorage

NSHTTPCookieStorage类采用单例的设计模式,其中管理着所有HTTP请求的Cookie信息

**官方解释:**NSHTTPCookieStorage 是一个用来管理 cookie 存储的单例。一个 NSHTTPCookie 单例代表一个 cookie。通常来讲,cookie 可以在应用间共享,并且在进程之间保持同步。 对于单进程,Session cookies (这里的 cookie 对象的 isSessionOnly 方法返回 YES)是局部的并且不能被共享。

常用方法

// 获取单例对象
+ (NSHTTPCookieStorage *)sharedHTTPCookieStorage;
 
// 所有Cookie数据数组 其中存放NSHTTPCookie对象
@property (nullable , readonly, copy) NSArray<NSHTTPCookie *> *cookies;
 
// 手动设置一条Cookie数据
- (void)setCookie:(NSHTTPCookie *)cookie;
 
// 删除某条Cookie信息
- (void)deleteCookie:(NSHTTPCookie *)cookie;
 
// 删除某个时间后的所有Cookie信息 iOS8后可用
- (void)removeCookiesSinceDate:(NSDate *)date NS_AVAILABLE(10_10, 8_0);
 
// 获取某个特定URL的所有Cookie数据
- (nullable NSArray<NSHTTPCookie *> *)cookiesForURL:(NSURL *)URL;
 
// 为某个特定的URL设置Cookie
- (void)setCookies:(NSArray<NSHTTPCookie *> *)cookies forURL:(nullable NSURL *)URL mainDocumentURL:(nullable NSURL *)mainDocumentURL;
 
// Cookie数据的接收协议
枚举如下:
typedef NS_ENUM(NSUInteger, NSHTTPCookieAcceptPolicy) {
    NSHTTPCookieAcceptPolicyAlways,//接收所有Cookie信息
    NSHTTPCookieAcceptPolicyNever,//不接收所有Cookie信息
    NSHTTPCookieAcceptPolicyOnlyFromMainDocumentDomain//只接收主文档域的Cookie信息
};
@property NSHTTPCookieAcceptPolicy cookieAcceptPolicy;
 
//系统下面的两个通知与Cookie管理有关:
// Cookie数据的接收协议改变时发送的通知
FOUNDATION_EXPORT NSString * const NSHTTPCookieManagerAcceptPolicyChangedNotification;
// 管理的Cookie数据发生变化时发送的通知
FOUNDATION_EXPORT NSString * const NSHTTPCookieManagerCookiesChangedNotification;

NSHTTPCookie介绍

NSHTTPCookie是具体的HTTP请求Cookie数据对象.

// 下面两个方法用于对象的创建和初始化 都是通过字典进行键值设置
- (nullable instancetype)initWithProperties:(NSDictionary<NSString *, id> *)properties;
+ (nullable NSHTTPCookie *)cookieWithProperties:(NSDictionary<NSString *, id> *)properties;
 
// 返回Cookie数据中可用于添加HTTP头字段的字典
+ (NSDictionary<NSString *, NSString *> *)requestHeaderFieldsWithCookies:(NSArray<NSHTTPCookie *> *)cookies;
 
// 从指定的响应头和URL地址中解析出Cookie数据
+ (NSArray<NSHTTPCookie *> *)cookiesWithResponseHeaderFields:(NSDictionary<NSString *, NSString *> *)headerFields forURL:(NSURL *)URL;
 
// Cookie数据中的属性字典
@property (nullable, readonly, copy) NSDictionary<NSString *, id> *properties;
 
// 请求响应的版本
@property (readonly) NSUInteger version;
 
// 请求相应的名称
@property (readonly, copy) NSString *name;
 
// 请求相应的值
@property (readonly, copy) NSString *value;
 
// 过期时间
@property (nullable, readonly, copy) NSDate *expiresDate;
 
// 请求的域名
@property (readonly, copy) NSString *domain;
 
//请求的路径
@property (readonly, copy) NSString *path;
 
// 是否是安全传输
@property (readonly, getter=isSecure) BOOL secure;
 
// 是否只发送HTTP的服务
@property (readonly, getter=isHTTPOnly) BOOL HTTPOnly;
 
// 响应的文档
@property (nullable, readonly, copy) NSString *comment;
 
// 相应的文档URL
@property (nullable, readonly, copy) NSURL *commentURL;
 
// 服务端口列表
@property (nullable, readonly, copy) NSArray<NSNumber *> *portList;
 
  • UIWebView的 Cookie 机制

UIWebView 在浏览网页后会将网页中的 cookie 自动存入 NSHTTPCookieStorage 标准容器中,[NSHTTPCookieStorage sharedHTTPCookieStorage]这个单例管理,在后续访问中会将 cookie 自动带到 request 请求当中。并且在同一个app内多个UIWebView之间共享。

  • WKWebView 的 Cookie 机制

NSURLCache和NSHTTPCookieStroage无法操作(WKWebView)WebCore进程的缓存和Cookie WKWebView实例将会忽略任何的默认网络存储器(NSURLCache, NSHTTPCookieStorage, NSCredentialStorage) 和一些标准的自定义网络请求类(NSURLProtocol,等等.),WKWebView实例不会把Cookie存入到App标准的的Cookie容器(NSHTTPCookieStorage)中,因为 NSURLSession/NSURLConnection等网络请求使用NSHTTPCookieStorage进行访问Cookie,所以不能访问WKWebView的Cookie,现象就是WKWebView存了Cookie,其他的网络类如NSURLSession/NSURLConnection却看不到。这是很多人的说法。 还有一种是说法是通过实践发现 WKWebView 实例其实也会将 Cookie 存储于 NSHTTPCookieStorage 中,但存储时机有延迟,因为WKWebView内也有cookie的容器,而且每隔一段时间就和app侧NSHTTPCookieStorage进行同步,而且这个同步是进程级别的同步,而且这个同步是单向。 至于以上两种说法,最终WKWebView Cookie 问题在于 WKWebView 发起的请求不会自动带上存储于 NSHTTPCookieStorage 容器中的 Cookie。

  • iOS11 iOS11 的 API 可以解决该问题,只要是存在 WKHTTPCookieStore 里的 cookie,WKWebView 每次请求都会携带,存在 NSHTTPCookieStorage 的cookie,并不会每次都携带。于是会发生首次 WKWebView 请求不携带 Cookie 的问题。
  • ios 11 WKWebView cookie 的注入 在执行 -[WKWebView loadReques:] 前将 NSHTTPCookieStorage 中的内容复制到 WKHTTPCookieStore 中,以此来达到 WKWebView Cookie 注入的目的。示例代码如下:
[self copyNSHTTPCookieStorageToWKHTTPCookieStoreWithCompletionHandler:^{
            NSURL *url = [NSURL URLWithString:@"https://www.v2ex.com"];
            NSURLRequest *request = [NSURLRequest requestWithURL:url];
            [_webView loadRequest:request];
        }];

- (void)copyNSHTTPCookieStorageToWKHTTPCookieStoreWithCompletionHandler:(nullable void (^)())theCompletionHandler; {
    NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
    WKHTTPCookieStore *cookieStroe = self.webView.configuration.websiteDataStore.httpCookieStore;
    if (cookies.count == 0) {
        !theCompletionHandler ?: theCompletionHandler();
        return;
    }
    for (NSHTTPCookie *cookie in cookies) {
        [cookieStroe setCookie:cookie completionHandler:^{
            if ([[cookies lastObject] isEqual:cookie]) {
                !theCompletionHandler ?: theCompletionHandler();
                return;
            }
        }];
    }
}

ios11 之前Cookie注入

注入 Cookie 就是从之前保存 cookie 的 NSHTTPCookieStorage 中取出相关 Cookie,然后在再次请求访问的时候在 request 中注入 Cookie。注入Cookie同样有多种方式。

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];

JS注入2

- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
    [webView evaluateJavaScript:@"document.cookie ='TeskCookieKey1=TeskCookieValue1';" completionHandler:^(id result, NSError *error) {
        //...
    }];
}
 

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;
}

重定向302

解决方案1:对比跳转URL与页面URL的host,如果为跨域,通过WKUserScript在注入cookie信息(document.cookie)。

/* 跨域重定向-同步cookie */
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    NSString* URLString = ([self.crossDominLink isNotEmpty] ? self.crossDominLink: self.pageUrl);
    NSURL* currentURL = [NSURL URLWithString:URLString];
 
    if (![currentURL.host isEqualToString:navigationAction.request.URL.host]) {
        //跨域重定向注入cookieScript
        NSString *strDocumentCookie = [MKCookieManager genDocumentCookieForWKWebview:navigationAction.request.URL];
        WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource:strDocumentCookie injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
        [webView.configuration.userContentController  addUserScript:cookieScript];
 
        self.crossDominLink = navigationAction.request.URL.absoluteString;
    }
    decisionHandler(WKNavigationActionPolicyAllow);
}

解决方案2:对比跳转URL与页面URL的host,如果为跨域且未做处理,拦截该跳转,重新配置request的cookie信息,再loadRequest一下。(只适合加载mainFrame请求,iframe无效)

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    NSURL* currentURL = [NSURL URLWithString:self.pageUrl];
    
    if ((![self.crossDomainURL isEqualToString:navigationAction.request.URL.absoluteString]) && (![currentURL.host isEqualToString:navigationAction.request.URL.host])) {
 
        self.crossDomainURL = navigationAction.request.URL.absoluteString;
 
        NSMutableURLRequest* request = [navigationAction.request mutableCopy];
 
        NSString* cookieValue = @"session=720bff297ea3866696380368f81fc4e4;token_id=NzIwYmZmMjk3ZWEzODY2Njk2MzgwMzY4ZjgxZmM0ZTQ=;uid=123456;";
 
        [request setValue:cookieValue forHTTPHeaderField:@"Cookie"];
 
        [webView loadRequest:request];
 
        decisionHandler(WKNavigationActionPolicyCancel);
    } else {
        decisionHandler(WKNavigationActionPolicyAllow);
    }
    
}

清除 cookie


- (void)clearCacheAndCookie {  
    //清除cookies  
    NSHTTPCookie *cookie;  
    NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage];  
    for (cookie in [storage cookies]) {  
        [storage deleteCookie:cookie];  
    }  
      
    [[NSURLCache sharedURLCache] removeAllCachedResponses];  
    NSURLCache * cache = [NSURLCache sharedURLCache];  
    [cache removeAllCachedResponses];  
    [cache setDiskCapacity:0];  
    [cache setMemoryCapacity:0];  
      
    WKWebsiteDataStore *dataStore = [WKWebsiteDataStore defaultDataStore];  
    [dataStore fetchDataRecordsOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] completionHandler:^(NSArray<WKWebsiteDataRecord *> * _Nonnull records) {  
        for (WKWebsiteDataRecord *record in records) {  
            [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:record.dataTypes forDataRecords:@[record] completionHandler:^{  
                NSLog(@"WKWebView清除缓存 ========= Cookies for %@ deleted successfully",record.displayName);  
            }];  
        }  
    }];  
}  

let recordType = WKWebsiteDataStore.allWebsiteDataTypes()
WKWebsiteDataStore.default().fetchDataRecords(ofTypes: recordType) { records in
    WKWebsiteDataStore.default().removeData(ofTypes: recordType, for: records) {
    }
}

相关参考链接

www.iteye.com/blog/wy1992…

www.codeleading.com/article/255…

www.jianshu.com/p/4ae4fd25a…

blog.csdn.net/z119901214/…

ask.csdn.net/questions/6…