IOS进阶:(网络篇)HTTPCookie

2020-10-28  本文已影响0人  时光啊混蛋_97boy

原创:知识进阶型文章
无私奉献,为国为民,创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

目录

一、Cookie

百度首页Cookie

1、什么是Cookie

Cookie是由服务器端生成,发送给User-Agent(一般是浏览器或者客户端),浏览器会将Cookie的key/value保存到某个目录下的文本文件内,下次请求同一网站地址时就发送该Cookie给服务器。Cookie必然会通过HTTPRespone传过来,并且CookieRespone中的HTTP header中。

2、为什么需要Cookie?

HTTP是一种无状态的协议,客户端与服务器建立连接并传输数据,数据传输完成后,连接就会关闭。再次交互数据需要建立新的连接,因此,服务器无法从连接上跟踪会话,也无法知道用户上一次做了什么。这严重阻碍了基于Web应用程序的交互,也影响用户的交互体验。如:在网络有时候需要用户登录才进一步操作,用户输入用户名密码登录后,浏览了几个页面,由于HTTP的无状态性,服务器并不知道用户有没有登录。

Cookie是解决HTTP无状态性的有效手段,服务器可以设置或读取Cookie中所包含的信息。当用户登录后,服务器会发送包含登录凭据的Cookie到用户浏览器客户端,而浏览器对该Cookie进行某种形式的存储(内存或硬盘)。用户再次访问该网站时,浏览器会发送该CookieCookie未到期时)到服务器,服务器对该凭据进行验证,合法时使用户不必输入用户名和密码就可以直接登录。

实际项目中使用场景如:当Native端用户是登录状态的,打开一个h5页面,h5也要维持用户的登录状态。这个需求看似简单,如何实现呢?一般的解决方案是Native保存登录状态的Cookie,在打开h5页面中,把Cookie添加上,以此来维持登录状态。其实坑还是有很多的,比如用户登录或者退出了,h5页面的登录状态也变了,需要刷新,什么时候刷新?WKWebView中Cookie丢失问题?

3、cookie的类型

Cookie总时由用户客户端进行保存的(一般是浏览器),按其存储位置可分为:内存式Cookiecookie是指在不设定它的生命周期expires时的状态)和硬盘式Cookie

内存式Cookie存储在内存中,浏览器关闭后就会消失,由于其存储时间较短,因此也被称为非持久Cookie或会话Cookie
硬盘式Cookie保存在硬盘中,其不会随浏览器的关闭而消失,除非用户手工清理或到了过期时间。由于硬盘式Cookie存储时间是长期的,因此也被称为持久Cookie

4、cookie实现原理

cookie定义了一些HTTP请求头和HTTP响应头,通过这些HTTP头信息使服务器可以与客户进行状态交互。

客户端请求服务器后,如果服务器需要记录用户状态,服务器会在响应信息中包含一个Set-Cookie的响应头,客户端会根据这个响应头存储Cookie信息。再次请求服务器时,客户端会在请求信息中包含一个Cookie请求头,而服务器会根据这个请求头进行用户身份、状态等较验。

5、与session的区别

cookie机制采用的是在客户端保持状态的方案,而session机制采用的是在服务器端保持状态的方案。由于采用服务器端保持状态的方案在客户端也需要保存一个标识,所以session机制也需要借助于cookie机制来达到保存标识的目的。

6、iOS中的Cookie

当你访问一个网站时,NSURLRequest都会帮你主动记录下来你访问的站点设置的Cookie,如果Cookie 存在的话,会把这些信息放在 NSHTTPCookieStorage容器中共享,当你下次再访问这个站点时,NSURLRequest会拿着上次保存下来了的Cookie继续去请求。

所以UIWebView的Cookie管理很简单,一般不需要我们手动操作Cookie,全部Cookie都会被[NSHTTPCookieStorage sharedHTTPCookieStorage]这个单例管理,而且UIWebView会自动同步CookieStorage中的Cookie,所以只要我们在Native端,正常登陆退出,h5在适当时候刷新,就可以正确的维持登录状态,不需要做多余的操作。

二、Demo实战

1、获得UIWebView的Cookies

实现webViewCookiesButton的调用方法webViewCookies:

- (void)webViewCookies
{
    // 创建新的UIWebView
    self.webView = [[UIWebView alloc] initWithFrame:CGRectMake(0, 400, self.view.bounds.size.width, 600)];
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.baidu.com"]];
    [self.webView loadRequest:request];
    [self.view addSubview:self.webView];
    
    // 打印出所有cookie信息
    NSHTTPCookieStorage *storages = [NSHTTPCookieStorage sharedHTTPCookieStorage];
    for (NSHTTPCookie *cookie in [storages cookies])
    {
        NSLog(@"%@",cookie);
    }
}

又到了知识小课堂的时间
NSHTTPCookieNSHTTPCookie对象代表一个HTTP cookie。 这是一个不可改变的对象,从一个包含cookie的属性的字典初始化,这个类可以用来手动创建cookieProperties

//下面两个方法用于对象的创建和初始化 都是通过字典进行键值设置
- (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;

NSHTTPCookieStorageNSHTTPCookieStorage类采用单例的设计模式,其中管理着所有HTTP请求的Cookie信息,更改cookie的接收政策将会影响当前所有正在使用cookieapp

//获取单例对象
+ (NSHTTPCookieStorage *)sharedHTTPCookieStorage;    

//所有Cookie数据数组 其中存放NSHTTPCookie对象
@property (nullable , readonly, copy) NSArray<NSHTTPCookie *> *cookies;   

//手动设置一条Cookie数据
- (void)setCookie:(NSHTTPCookie *)cookie;   

//删除某条Cookie信息
- (void)deleteCookie:(NSHTTPCookie *)cookie;    

//获取某个特定URL的所有Cookie数据
- (nullable NSArray<NSHTTPCookie *> *)cookiesForURL:(NSURL *)URL;    

//删除某个时间后的所有Cookie信息 iOS8后可用
- (void)removeCookiesSinceDate:(NSDate *)date NS_AVAILABLE(10_10, 8_0);    

//为某个特定的URL设置Cookie
- (void)setCookies:(NSArray<NSHTTPCookie *> *)cookies forURL:(nullable NSURL *)URL mainDocumentURL:(nullable NSURL *)mainDocumentURL

/*
枚举如下:
typedef NS_ENUM(NSUInteger, NSHTTPCookieAcceptPolicy) {
    NSHTTPCookieAcceptPolicyAlways,//接收所有Cookie信息
    NSHTTPCookieAcceptPolicyNever,//不接收所有Cookie信息
    NSHTTPCookieAcceptPolicyOnlyFromMainDocumentDomain//只接收主文档域的Cookie信息
};
*/
@property NSHTTPCookieAcceptPolicy cookieAcceptPolicy;//Cookie数据的接收协议

**系统下面的两个通知与Cookie管理有关**

//Cookie数据的接收协议改变时发送的通知
FOUNDATION_EXPORT NSString * const NSHTTPCookieManagerAcceptPolicyChangedNotification;
//管理的Cookie数据发生变化时发送的通知
FOUNDATION_EXPORT NSString * const NSHTTPCookieManagerCookiesChangedNotification;

**存放和获取一个task任务所对应的cookie,iOS8.0以后支持**
- (void)storeCookies:(NSArray<NSHTTPCookie *> *)cookies forTask:(NSURLSessionTask *)task NS_AVAILABLE(10_10, 8_0);
- (void)getCookiesForTask:(NSURLSessionTask *)task completionHandler:(void (^) (NSArray<NSHTTPCookie *> * _Nullable cookies))completionHandler NS_AVAILABLE(10_10, 8_0);

看看运行的结果打印出来的Cookie是怎样的...

点击webViewCookiesButton
Cookie结构.png
需要注意的是Cookie在在iOS中不会多应用共享,但是会在不同进程之间保持同步,Session Cookie(一个isSessionOnly方法返回YESCookie)只能在单一进程中使用。至于其他属性,在之前介绍NSHTTPCookie有提到。

2、设置UIWebView的Cookies

a、首先我们需要实现一个设置新Cookies的方法来对Cookies的各项属性值进行设置。

- (void)setCookieWithDomain:(NSString*)domainValue
                sessionName:(NSString *)name
               sessionValue:(NSString *)value
                expiresDate:(NSDate *)date

其中对各项属性值进行设置的部分如下:

    // 创建字典存储cookie的属性值
    NSMutableDictionary *cookieProperties = [NSMutableDictionary dictionary];
    // 设置cookie名
    [cookieProperties setObject:name forKey:NSHTTPCookieName];
    // 设置cookie值
    [cookieProperties setObject:value forKey:NSHTTPCookieValue];
    
    // 设置cookie域名
    NSURL *url = [NSURL URLWithString:domainValue];
    NSString *domain = [url host];
    [cookieProperties setObject:domain forKey:NSHTTPCookieDomain];
    
    // 设置cookie路径 一般写"/"
    [cookieProperties setObject:@"/" forKey:NSHTTPCookiePath];
    // 设置cookie版本, 默认写0
    [cookieProperties setObject:@"0" forKey:NSHTTPCookieVersion];
    
    //设置cookie过期时间
    if (date)
    {
        [cookieProperties setObject:date forKey:NSHTTPCookieExpires];
    }
    else
    {
        // 推迟一年
        NSDate *date = [NSDate dateWithTimeIntervalSince1970:([[NSDate date] timeIntervalSince1970] + 365*24*3600)];
        [cookieProperties setObject:date forKey:NSHTTPCookieExpires];
    }

因为手动设置的Cookie不会自动持久化到沙盒,所以需要我们自己来实现

    // 设置cookie的属性值到本地磁盘,因为手动设置的Cookie不会自动持久化到沙盒
    [[NSUserDefaults standardUserDefaults] setObject:cookieProperties forKey:@"app_cookies"];

接着在添加新的cookie之前,我们还需要删除掉原来的cookie

    // 删除原cookie, 如果存在的话
    NSArray * arrayCookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
    for (NSHTTPCookie *cookice in arrayCookies)
    {
        // 清除特定某个cookie可以加个判断: if ([cookie.name isEqualToString:@"cookiename"])
        [[NSHTTPCookieStorage sharedHTTPCookieStorage] deleteCookie:cookice];
    }

使用字典初始化新的cookie

NSHTTPCookie *newcookie = [NSHTTPCookie cookieWithProperties:cookieProperties];

最后使用cookie管理器存储cookie

[[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:newcookie];

补充一点,如果我们想清除某一个url缓存,可以这样来做:

[NSURLCache sharedURLCache] removeCachedResponseForRequest:[NSURLRequest requestWithURL:url];

b、实现setWebViewCookiesButtonsetWebViewCookies方法

- (void)setWebViewCookies
{
    // 设置新Cookies
    [self setCookieWithDomain:@"http://www.baidu.com" sessionName:@"xiejiapei_token_UIWebView" sessionValue:@"55555555" expiresDate:nil];
    
    // 取出刚设置的新cookie
    NSArray *cookiesArray = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
    NSDictionary *headerCookieDict = [NSHTTPCookie requestHeaderFieldsWithCookies:cookiesArray];
    
    // 设置请求头
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://www.baidu.com"]];
    request.allHTTPHeaderFields = headerCookieDict;
    [self.webView loadRequest:request];
}

c、运行APP验证下我们的Demo效果

创建了新cookie,设置了其属性后存储下来

创建了新`cookie`

取出刚设置的新cookie,将其设置为请求头

取出刚设置的新cookie将其设置为了请求头

实际运行后,通过Charles捕获网络请求,在状态码为302的请求的Content中我们看到确实存储了刚才自己设置的cookie,并且在本地沙盒Preferences中,打开.plist文件,cookie也成功保存到了本地

image.png

点击webViewCookiesButton后,相应的控制台也的确打印出了我们设置的cookie

image.png

3、获取WKWebView的Cookies

接下来的过程可能有点绕,最初我也更整懵了......大家要做好心理准备。不知道苹果为什么给WKWebView设置了这么一个坑?原谅我才疏学浅不懂原因,要不是看了大家的文章,都不知道还有这种鬼问题。

UIWebViewCookie是通过 NSHTTPCookieStorage统一管理,服务器返回时写入,发起请求时读取,WebNative 通过该对象能共享 Cookie

说起WKWebview代替UIWebview带来的好处你可以举出一堆堆的例子,但说到 WKWebview的问题,除了WKWebview视图尺寸问题,默认跳转被屏蔽,需要手动交互之外,你绕不过的就是WKWebview cookieNSHTTPCookieStorage cookie不共享的问题。
如何将 NSHTTPCookieStorage 同步给WKWebview,大概要处理很多种情况:

1、初次加载页面时,同步 cookie 到 WKWebview
2、处理 ajax 请求时,需要的 cookie
3、如果 response 里有 set-cookie 还需要缓存这些 cookie
4、如果是 302新页面跳转 还需要处理 cookie 传递的问题

那么我们不禁好奇为什么NSHTTPCookieStorageWKWebview 没有同步呢?首先来看看WKWebview cookie是怎么存储的?

未过期的cookie存储位置

为了验证,你可以打开这两者文件进行查看: 当然两个文件都是 binary file,直接用文本浏览器打开是看不到,有一个python写的脚本 BinaryCookieReaderhttps://gist.github.com/sh1n0b1/4bb8b737370bfe5f5ab8。可以读出来,我不怎么懂python,就不展开了...

明白了存储方式,让我们来思考🤔下WKWebview Cookie究竟是如何工作的?

系统默认方式:
webview loadRequest 或者 302重定向 或者在 webview 加载完毕,触发了 ajax请求时,WKWebview所需的 Cookie 会去 Cookie.binarycookies 里读取本域名下的 Cookie,加上
WKProcessPool持有的Cookie 一起作为request 头里的Cookie 数据。

这种方式的问题是NSHTTPCookieStorageCookie 根本没有共享给 WKWebview,没有涉及到session暂不考虑WKProcessPool,因此导致request 头里的Cookie 数据为空,即allHTTPHeaderFields为空,这就是万恶之源啊啊啊啊😂~让我们实际验证下控制台输出结果。

引入#import <WebKit/WebKit.h>,声明会实现<WKNavigationDelegate>委托,实现wkWebViewCookiesButton的调用方法wkWebViewCookies

- (void)wkWebViewCookies
{
    // 创建新的WKWebView
    self.wkWebView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 400, self.view.bounds.size.width, 600)];
    self.wkWebView.navigationDelegate = self;
    [self.view addSubview:self.wkWebView];

    // 将cookie放在请求头里面
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://www.baidu.com"]];
    NSLog(@"request.allHTTPHeaderFields: %@",request.allHTTPHeaderFields);
    [self.wkWebView loadRequest:request];
}
allHTTPHeaderFields是空的
Charles没有捕获到Cookie信息
// 这是上面👆那一串完整的Cookie信息,可以看到没有我们自己设置的那部分信息
BAIDUID=B01696B5316606EBC8EFEADAF0444881:FG=1; H_WISE_SIDS=148077_149391_148504_143879_149356_150073_147087_141744_148193_148867_148435_147279_148824_149531_147638_148754_147897_146574_148523_149175_127969_146548_149329_149719_146652_147024_146732_138426_149558_149617_131423_100805_147527_107314_147136_148570_148185_147717_149251_146395_144966_149279_145607_139884_148048_148752_148869_146046_110085; BD_BOXFO=_avOi_aivYo7C; SE_LAUNCH=5%3A26542282_3%3A26542286; bd_af=1; BDORZ=AE84CDB3A529C0F8A2B9DCDD1D18B695

⚠️ :需要注意的是,并非说系统的NSHTTPCookieStorageWKWebView中所有Cookie都无法自动同步,两个存储文件完全各自为政。

既然发现了问题,接下来就要大刀阔斧地干了! (凶恶嘴脸😎)

问题一:解决首次加载Cookie带不上问题
这个比较简单,Cookies数组转换为requestHeaderFields,再将其设置为请求头即可,这样,只要你保证sharedHTTPCookieStorage中你的Cookie存在,首次访问一个页面,就不会有问题。

- (void)wkWebViewCookies
{
    // 创建新的WKWebView
    self.wkWebView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 400, self.view.bounds.size.width, 600)];
    self.wkWebView.navigationDelegate = self;
    [self.view addSubview:self.wkWebView];

    // 将cookie放在请求头里面
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://www.baidu.com"]];
    NSArray  *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
    // Cookies数组转换为requestHeaderFields
    NSDictionary *requestHeaderFields = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
    // 设置请求头
    request.allHTTPHeaderFields = requestHeaderFields;
    NSLog(@"request.allHTTPHeaderFields: %@",request.allHTTPHeaderFields);
    [self.wkWebView loadRequest:request];
}

看下运行效果,发现我们成功将其设置为了请求头,这样request.allHTTPHeaderFields就不为空了,并且Charles也捕获到了该Cookie信息。

设置Cookie作请求头
Charles也捕获到了该Cookie信息

问题二:解决跳转新页面时Cookie带不过去问题
心有余而力不足,真机调试又出幺蛾子了,折腾了十多分钟,证书这些配置的东西真烦人。以后有机会再补上效果图。

这里的问题是当你点击页面上的某个链接,跳转到新的页面,Cookie又丢了......好弱智啊......怎么解决呢?

需要注意的地方,如果navigationAction.requestNSURLRequest,不可变,那不就添加不了Cookie了,是的,但我们不能因为这个问题,不允许跳转。也不能在不允许跳转之后用loadRequest加载fixedRequest,否则会出现死循环。

a、新建了一个WKCookieManager工具类,用更安全的方式设置了一个单例来方便调用之后的方法。


// 单例
+ (instancetype)shareManager
{
    // 静态局部变量
    static WKCookieManager *_instance;
    // 通过dispatch_ once方式确保instance在多线程环境下只被创建一次
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 创建实例
        // super: 不能使用self,否则重写的allocWithZone第一次初始化的时候 会循环调用instance
        _instance = [[super allocWithZone:NULL] init];
    });
    return _instance;
}

// 重写方法[必不可少]
// 规避逃脱sharedInstance再去创建其他对象,当alloc的时候只能返回单例
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    return [self shareManager];
}

b、在.h文件里声明了fixNewRequestCookieWithRequest方法

/**
 解决新的跳转 Cookie 丢失问题
 @param originalRequest 拦截的请求
 @return 带上 Cookie 的新请求
 */
- (NSURLRequest *)fixNewRequestCookieWithRequest:(NSURLRequest *)originalRequest;

c、在.m文件中来实现该方法,首先需要注意的是如果navigationAction.requestNSURLRequest,不可变,那不就添加不了Cookie了,所以我们这里需要让它可变。其中因为传入是NSURLRequest,但是其实际类型为NSMutableURLRequest,我们就可以根据里氏替换原则对其进行运行时强制转化为子类。而当其为NSURLRequest `,只需要进行可变拷贝即可,为深拷贝。

    // 如果`navigationAction.request`是`NSURLRequest`,不可变,那不就添加不了`Cookie`了
    // 所以我们这里需要让它可变
    NSMutableURLRequest *fixedRequest;
    if ([originalRequest isKindOfClass:[NSMutableURLRequest class]])
    {
        // 里氏替换原则:父类可以被子类无缝替换,且原有功能不受影响
        // 例如:KVO实现原理,调用addObserver方法,系统在动态运行时候为我们创建一个子类,我们虽然感受到的是使用原有的父类,实际上是子类
        fixedRequest = (NSMutableURLRequest *)originalRequest;
    }
    else
    {
        // 只需要进行可变拷贝即可
        fixedRequest = originalRequest.mutableCopy;
    }

d、取出解决问题一时候的NSHTTPCookieStorage中的Cookie,并将其设置为fixedRequest.allHTTPHeaderFields,其实解决思路都一样,就是它没有那么就从保存下来的地方给它一个就好了。

    // 关键步骤:防止Cookie丢失
    // 前提是保证sharedHTTPCookieStorage中你的Cookie存在
    NSDictionary *dict = [NSHTTPCookie requestHeaderFieldsWithCookies:[NSHTTPCookieStorage sharedHTTPCookieStorage].cookies];
    if (dict.count)
    {
        NSMutableDictionary *mDict = originalRequest.allHTTPHeaderFields.mutableCopy;
        [mDict setValuesForKeysWithDictionary:dict];
        fixedRequest.allHTTPHeaderFields = mDict;
    }
    return fixedRequest;

打断点调试下,看是否能行,结果显示是OK的:

跳转新页面能拿到Cookie了

问题三:解决后续Ajax请求(局部页面更新请求)Cookie丢失问题

AJAX = Asynchronous JavaScript and XML(异步的 JavaScript 和 XML)。
AJAX 不是新的编程语言,而是一种使用现有标准的新方法。
AJAX 最大的优点是在不重新加载整个页面的情况下,可以与服务器交换数据并更新部分网页内容。
AJAX 不需要任何浏览器插件,但需要用户允许JavaScript在浏览器上执行。

解决此问题的关键是注入的 JS 代码块。
a、在.h文件里声明了fixNewRequestCookieWithRequest方法

/**
 Ajax请求(局部页面更新请求)Cookie 丢失问题
 @return 注入的 JS 代码块
 */
- (WKUserScript *)futhureCookieScript;

b、在.m文件中来实现该方法,此处需要注意forMainFrameOnly为NO,因为我们需要将Cookie注入到所有frames

// Ajax请求(局部页面更新请求)Cookie 丢失问题
- (WKUserScript *)futhureCookieScript
{
    // 只读属性,表示JS是否应该注入到所有的frames中还是只有main frame
    WKUserScript *cookieScript = [[WKUserScript alloc] initWithSource:[self cookieString] injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
    return cookieScript;
}

相应JS脚本如下:

- (NSString *)cookieString
{
    NSMutableString *script = [NSMutableString string];
    [script appendString:@"var cookieNames = document.cookie.split('; ').map(function(cookie) { return cookie.split('=')[0] } );\n"];
    for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]) {

        if ([cookie.value rangeOfString:@"'"].location != NSNotFound) {
            continue;
        }
        [script appendFormat:@"if (cookieNames.indexOf('%@') == -1) { document.cookie='%@'; };\n", cookie.name, cookie.xjp_formatCookieString];
    }
    return script;
}

此处需要写个将cookie格式化为string的扩展方法:

#import "NSHTTPCookie+Util.h"

@implementation NSHTTPCookie (Util)

// 将cookie格式化为string的扩展方法
- (NSString *)xjp_formatCookieString{
    NSString *string = [NSString stringWithFormat:@"%@=%@;domain=%@;path=%@",
                        self.name,
                        self.value,
                        self.domain,
                        self.path ?: @"/"];
    
    if (self.secure) {
        string = [string stringByAppendingString:@";secure=true"];
    }
    
    return string;
}

@end

c、接着在HTTPCookieViewController中调用我们刚才实现的方法,此时创建新的WKWebView需要采用configuration的初始化方式,为了向contoller中注入脚本

    // 创建新的WKWebView,该用configuration的初始化方式,为了向contoller中注入脚本
    WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
    WKUserContentController *contoller = [[WKUserContentController alloc] init];
    [contoller addUserScript:[[WKCookieManager shareManager] futhureCookieScript]];
    configuration.userContentController = contoller;
    self.wkWebView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 400, self.view.bounds.size.width, 600) configuration:configuration];
    self.wkWebView.navigationDelegate = self;
    [self.view addSubview:self.wkWebView];

大功告成,同样只要你保证sharedHTTPCookieStorage中你的Cookie存在,后续Ajax请求就不会有问题。

问题四:如果 response 里有 set-cookie 还需要缓存这些 cookie
保证sharedHTTPCookieStorage中你的Cookie存在。怎么保证呢?由于WKWebView加载网页得到的Cookie会同步到NSHTTPCookieStorage中的特点,有时候你强行添加的Cookie会在同步过程中丢失。Charles抓包发现点击一个链接时,Requestheader中多了Set-Cookie字段,其实Cookie已经丢了。

解决方案那就是把自己需要的Cookie主动保存起来,每次调用[NSHTTPCookieStorage sharedHTTPCookieStorage].cookies方法时,保证返回的数组中有自己需要的Cookie。下面上代码,用了runtimeMethod Swizzling

a、创建NSHTTPCookieStorage (CookieUtil)扩展方法文件,引入运行时#import <objc/runtime.h>框架,接着实现class_methodSwizzling替换方法:

/**
*  方法替换。Method Swizzling技术。使类中的方法实现和自己的方法实现互换,达到替换默认,且还可以调用默认方法的目的。
*
*  @param class            替换的方法所属的类
*  @param originalSelector 原始的方法选择器
*  @param swizzledSelector 用以替换的方法选择器
*/
static inline void class_methodSwizzling(Class class, SEL originalSelector, SEL swizzledSelector)
{
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    
    // 如果可以在原有类中添加方法,说明原有的类并没有实现,可能是继承自父类的方法。
    // 那么,我们添加一个方法,方法名为原方法名,实现为我们自己的实现。之后再将自己的方法替换成原始的实现。
    BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    
    //这么做,避免了替换方法时,由于本class中没有实现,从而替换了父类的方法。造成不可预知的错误。
    if (didAddMethod)
    {
        class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    }
    // 如果类中已经实现了这个原始方法,那么就与我们的方法互换一下实现即可。
    else
    {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

b、接着需要在load方法中调用我们的替换方法,将cookies的GET方法替换为我们自定义的custom_cookiesGet方法:

// 加载
+ (void)load
{
    class_methodSwizzling(self, @selector(cookies), @selector(custom_cookies));
}

c、于是我们需要实现一下这个自定义的Get方法custom_cookies

// 自定义cookies
- (NSArray<NSHTTPCookie *> *)custom_cookies
{
    // 获取到之前的所有cookies
    NSArray *cookies = [self custom_cookies];
    BOOL isExist = NO;
    
    // 寻找Custom_Client_Cookie
    for (NSHTTPCookie *cookie in cookies)
    {
        if ([cookie.name isEqualToString:@"Custom_Client_Cookie"])
        {
            isExist = YES;
            break;
        }
    }
    
    // 寻找不到则向CookieStroage中添加
    if (!isExist)
    {
        // 添加到NSHTTPCookieStorage,其中fetchAccessTokenCookie为创建新Cookie的方法
        NSHTTPCookie *cookie = [self fetchAccessTokenCookie];
        [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
        
        // 添加到返回数组中
        NSMutableArray *mutableCookies = cookies.mutableCopy;
        [mutableCookies addObject:cookie];
        cookies = mutableCookies.copy;
    }
    
    return cookies;
}

d、如果NSHTTPCookieStorage没有我们想要的Cookie,就需要我们创建一个,创建新CookiefetchAccessTokenCookie方法如下:

// 创建新Cookie
- (NSHTTPCookie *)fetchAccessTokenCookie
{
    NSMutableDictionary *properties = [NSMutableDictionary dictionary];
    [properties setObject:@"Custom_Client_Cookie" forKey:NSHTTPCookieName];
    [properties setObject:@"Cooci" forKey:NSHTTPCookieValue];
    [properties setObject:@"" forKey:NSHTTPCookieDomain];
    [properties setObject:@"/" forKey:NSHTTPCookiePath];
    NSHTTPCookie *accessCookie = [[NSHTTPCookie alloc] initWithProperties:properties];
    return accessCookie;
}

e、接下来需要在合适的时候(如登录成功)保存Cookie,实现该方法后,在viewDidLoad中调用

// 在合适的时候(如登录成功)保存Cookie
- (void)saveCookie
{
    NSArray *allCookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
    for (NSHTTPCookie *cookie in allCookies)
    {
        // 找到Custom_Client_Cookie
        if ([cookie.name isEqualToString:@"Custom_Client_Cookie"])
        {
            NSDictionary *dict = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"Custom_Client_Cookie"];
            if (dict)
            {
                // 本地Cookie有更新
                NSHTTPCookie *localCookie = [NSHTTPCookie cookieWithProperties:dict];
                if (![cookie.value isEqual:localCookie.value])
                {
                    NSLog(@"本地Cookie有更新");
                }
            }
            
            // 更新保存
            [[NSUserDefaults standardUserDefaults] setObject:cookie.properties forKey:@"Custom_Client_Cookie"];
            [[NSUserDefaults standardUserDefaults] synchronize];
            break;
        }
    }
}

看看运行结果如何?
运行后首先会进入方法交换方法class_methodSwizzling

class_methodSwizzling

进入HTTPCookieViewController页面后马上会进入saveCookie方法,由于NSArray *allCookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];调用了cookies的Get方法,所以又立刻进入到custom_cookies中,第一次因为不存在自定义Cookies需要进行创造并存储,所以mutableCookies拥有两个与元素,而cookie却拥有一个。

custom_cookies

最后又重新进入到saveCookie方法,将以前保存的本地Cookie和我们刚刚新设置的custom_cookies的值进行比较,我第一次设置的是linning,第二次设置为xiejiapei,因为两次不相等,所以输出cookies的值更新了。

saveCookie

拓展:Cookie 污染问题

原因:如果我们自己设置了 allHTTPHeaderFields,则系统不会使用 the cookie manager by default
解决方案:所以我们的方案是在页面加载过程中不去设置 allHTTPHeaderFields,全部使用默认 Cookie mananger管理,这样就不会有 Cookie 污染也不会有302 Cookie丢失的问题了。
唯一的问题:如何将 NSHTTPCookieStorageCookie共享给WKWebview
`

实践过程如下

在首次加载 url时,检查是否已经同步过 Cookie。如果没有同步过,则先加载 一个 cookieWebivew,它的主要目的就是将 Cookie先使用 usercontroller 的方式写到WKWebview里,这样在处理正式的请求时,就会带上我们从NSHTTPCookieStorage 获取到的 Cookie了。核心代码如下:

if ([AppHostCookie loginCookieHasBeenSynced] == NO) {
        //
        NSURL *cookieURL = [NSURL URLWithString:kFakeCookieWebPageURLString];
        NSMutableURLRequest *mutableRequest = [NSMutableURLRequest requestWithURL:cookieURL cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:120];
        WKWebView *cookieWebview = [self getCookieWebview];
        [self.view addSubview:cookieWebview];
        [cookieWebview loadRequest:mutableRequest];
        DDLogInfo(@"[JSBridge] preload cookie for url = %@", self.loadUrl);
    } else {
        [self loadWebPage];
    }
//  注意,CookieWebview 和 正常的 webview 是不同的
- (WKWebView *)getCookieWebview
{
    // 设置加载页面完毕后,里面的后续请求,如 xhr 请求使用的cookie
    WKUserContentController *userContentController = [WKUserContentController new];

    WKWebViewConfiguration *webViewConfig = [[WKWebViewConfiguration alloc] init];
    webViewConfig.userContentController = userContentController;

    webViewConfig.processPool = [AppHostCookie sharedPoolManager];
    
    NSMutableArray<NSString *> *oldCookies = [AppHostCookie cookieJavaScriptArray];
    [oldCookies enumerateObjectsUsingBlock:^(NSString *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) {
        NSString *setCookie = [NSString stringWithFormat:@"document.cookie='%@';", obj];
        WKUserScript *cookieScript = [[WKUserScript alloc] initWithSource:setCookie injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES];
        [userContentController addUserScript:cookieScript];
    }];

    WKWebView *webview = [[WKWebView alloc] initWithFrame:CGRectMake(0, -1, SCREEN_WIDTH,ONE_PIXEL) configuration:webViewConfig];

    webview.navigationDelegate = self;
    webview.UIDelegate = self;

    return webview;
}

这里需要处理的问题是,加载完毕或者失败后需要清理旧 webview和设置标记位。

static NSString * _Nonnull kFakeCookieWebPageURLString = @"http://ai.api.com/xhr/user/getUid.do?26u-KQa-fKQ-3BD"
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
{

    NSURL *targetURL = webView.URL;
    if ([AppHostCookie loginCookieHasBeenSynced] == NO && targetURL.query.length > 0 && [kFakeCookieWebPageURLString containsString:targetURL.query]) {
        [AppHostCookie setLoginCookieHasBeenSynced:YES];
        // 加载真正的页面;此时已经有 App 的 cookie 存在了。
        [webView removeFromSuperview];
        [self loadWebPage];
        return;
    }
}

同时记得删掉原来对 webviewCookie 的所有处理的代码。
处理至此,大功告成,这样的后续请求, WKWebview都用自身所有的CookieNSHTTPCookieStorageCookie,这样既达到了 Cookie 共享的目的,WKWebviewNSHTTPCookieStorageCookie也做了个隔离。

这个方法,我看得懵懵懂懂,大家想要深入研究的话,在这个开源项目 https://github.com/hite/AppHostExample/ 里有使用举例,具体的代码写在 https://github.com/hite/AppHost 这个库里。

更新:iOS 11后双向同步cookie简便方式

没亲自尝试过,先贴在这儿,以后试下,写下流程。
.h文件:

//
//  UWWkWebViewCookieManager.h
//
//  Created by DarkAngel on 2018/4/12.
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN
/**
 WKWebView的Cookie管理,只用于iOS 11以上
 */
@interface UWWkWebViewCookieManager : NSObject
/**
 从NSHTTPCookieStorage同步cookie
 */
+ (void)synchronizeCookiesFromNSHTTPCookieStorage NS_AVAILABLE_IOS(11_0);

@end

NS_ASSUME_NONNULL_END

.m文件:

//
//  UWWkWebViewCookieManager.m
//
//  Created by DarkAngel on 2018/4/12.
//

#import "UWWkWebViewCookieManager.h"
#import <WebKit/WebKit.h>
#import "GCDMethods.h"

@interface UWWkWebViewCookieManager () <WKHTTPCookieStoreObserver>

@end

@implementation UWWkWebViewCookieManager

+ (void)load
{
    if (@available(iOS 11.0, *)) {
        [[[WKWebsiteDataStore defaultDataStore] httpCookieStore] addObserver:(id<WKHTTPCookieStoreObserver>)self];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(cookiesDidChangeInHTTPCookieStorage:) name:NSHTTPCookieManagerCookiesChangedNotification object:nil];
    }
}

/**
 从[NSHTTPCookieStorage sharedHTTPCookieStorage]同步Cookie到WKHTTPCookieStore
 */
+ (void)synchronizeCookiesFromNSHTTPCookieStorage NS_AVAILABLE_IOS(11_0)
{
    if (@available(iOS 11.0, *)) {
        GCD_MAIN_SYNC(^{
            [[[WKWebsiteDataStore defaultDataStore] httpCookieStore] getAllCookies:^(NSArray<NSHTTPCookie *> * _Nonnull wkCookies) {
                NSMutableSet *before = [NSMutableSet setWithArray:wkCookies];
                NSSet *after = [NSSet setWithArray:[NSHTTPCookieStorage sharedHTTPCookieStorage].cookies];
                //需要保留的
                NSMutableSet *toKeep = [NSMutableSet setWithSet:before];
                [toKeep intersectSet:after];
                //需要添加的
                NSMutableSet *toAdd = [NSMutableSet setWithSet:after];
                [toAdd minusSet:toKeep];
                //需要删除的
                NSMutableSet *toRemove = [NSMutableSet setWithSet:before];
                [toRemove minusSet:after];
                for (NSHTTPCookie *cookie in toRemove.allObjects) {
                    [[[WKWebsiteDataStore defaultDataStore] httpCookieStore] deleteCookie:cookie completionHandler:nil];
                }
                for (NSHTTPCookie *cookie in toAdd.allObjects) {
                    [[[WKWebsiteDataStore defaultDataStore] httpCookieStore] setCookie:cookie completionHandler:nil];
                }
            }];
        });
    } else {
        
    }
}

/**
 从WKHTTPCookieStore同步Cookie到[NSHTTPCookieStorage sharedHTTPCookieStorage]
 */
+ (void)cookiesDidChangeInCookieStore:(WKHTTPCookieStore *)cookieStore NS_AVAILABLE_IOS(11_0)
{
    GCD_MAIN(^{
        [cookieStore getAllCookies:^(NSArray<NSHTTPCookie *> * _Nonnull cookies) {
            NSSet *before = [NSSet setWithArray:[NSHTTPCookieStorage sharedHTTPCookieStorage].cookies];
            NSMutableSet *after = [NSMutableSet setWithArray:cookies];
            //需要保留的
            NSMutableSet *toKeep = [NSMutableSet setWithSet:before];
            [toKeep intersectSet:after];
            //需要添加的
            NSMutableSet *toAdd = [NSMutableSet setWithSet:after];
            [toAdd minusSet:toKeep];
            for (NSHTTPCookie *cookie in toAdd.allObjects) {
                [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
            }
        }];
    });
}
/**
 从[NSHTTPCookieStorage sharedHTTPCookieStorage]同步Cookie到WKHTTPCookieStore
 */
+ (void)cookiesDidChangeInHTTPCookieStorage:(NSNotification *)notification
{
    if (@available(iOS 11.0, *)) {
        [self synchronizeCookiesFromNSHTTPCookieStorage];
    }
}

@end

Demo

Demo在我的Github上,欢迎下载。
NetworkAdvancedDemo

推荐Demo
iOS中UIWebView与WKWebView、JavaScript与OC交互、Cookie管理看我就够

参考文献

这才是 WKWebview Cookie 管理的正确方式
iOS中UIWebView与WKWebView、JavaScript与OC交互、Cookie管理看我就够

上一篇下一篇

猜你喜欢

热点阅读