原创:知识进阶型文章
无私奉献,为国为民,创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容
目录
- 一、设置Cookie
- 1、什么是Cookie
- 2、获得UIWebView的Cookies
- 3、设置UIWebView的Cookies
- 4、获取WKWebView的Cookies
- 二、解决WKWebView中的Cookies问题
- 1、解决首次加载Cookie带不上问题
- 2、解决跳转新页面时Cookie带不过去问题
- 问题三:解决后续Ajax请求(局部页面更新请求)Cookie丢失问题
- 问题四:如果 response 里有 set-cookie 还需要缓存这些 cookie
- 拓展:Cookie 污染问题
- 更新:iOS 11后双向同步cookie简便方式
- Demo
- 参考文献
一、设置Cookie


1、什么是Cookie
Cookie是由服务器端生成,发送给User-Agent(一般是浏览器或者客户端),浏览器会将Cookie的key/value保存到某个目录下的文本文件内,下次请求同一网站地址时就发送该Cookie给服务器。Cookie必然会通过HTTP的Respone传过来,并且Cookie在Respone中的HTTP header中。
为什么需要Cookie?
HTTP是一种无状态的协议,客户端与服务器建立连接并传输数据,数据传输完成后,连接就会关闭。再次交互数据需要建立新的连接,因此,服务器无法从连接上跟踪会话,也无法知道用户上一次做了什么。这严重阻碍了基于Web应用程序的交互,也影响用户的交互体验。如:在网络有时候需要用户登录才进一步操作,用户输入用户名密码登录后,浏览了几个页面,由于HTTP的无状态性,服务器并不知道用户有没有登录。
Cookie是解决HTTP无状态性的有效手段,服务器可以设置或读取Cookie中所包含的信息。当用户登录后,服务器会发送包含登录凭据的Cookie到用户浏览器客户端,而浏览器对该Cookie进行某种形式的存储(内存或硬盘)。用户再次访问该网站时,浏览器会发送该Cookie(Cookie未到期时)到服务器,服务器对该凭据进行验证,合法时使用户不必输入用户名和密码就可以直接登录。
实际项目中使用场景如:当Native端用户是登录状态的,打开一个h5页面,h5也要维持用户的登录状态。这个需求看似简单,如何实现呢?一般的解决方案是Native保存登录状态的Cookie,在打开h5页面中,把Cookie添加上,以此来维持登录状态。其实坑还是有很多的,比如用户登录或者退出了,h5页面的登录状态也变了,需要刷新,什么时候刷新?WKWebView中Cookie丢失问题?
cookie的类型
Cookie总是由用户客户端进行保存的(一般是浏览器),按其存储位置可分为:内存式Cookie(指在不设定它的生命周期expires时的状态)和硬盘式Cookie。内存式Cookie存储在内存中,浏览器关闭后就会消失,由于其存储时间较短,因此也被称为非持久Cookie或会话Cookie。硬盘式Cookie保存在硬盘中,其不会随浏览器的关闭而消失,除非用户手工清理或到了过期时间。由于硬盘式Cookie存储时间是长期的,因此也被称为持久Cookie。
cookie实现原理
cookie定义了一些HTTP请求头和HTTP响应头,通过这些HTTP头信息使服务器可以与客户端进行状态交互。客户端请求服务器后,如果服务器需要记录用户状态,服务器会在响应信息中包含一个Set-Cookie的响应头,客户端会根据这个响应头存储Cookie信息。再次请求服务器时,客户端会在请求信息中包含一个Cookie请求头,而服务器会根据这个请求头进行用户身份、状态等较验。
与session的区别
cookie机制采用的是在客户端保持状态的方案,而session机制采用的是在服务器端保持状态的方案。由于采用服务器端保持状态的方案在客户端也需要保存一个标识,所以session机制也需要借助于cookie机制来达到保存标识的目的。
iOS中的Cookie
当你访问一个网站时,NSURLRequest都会帮你主动记录下来你访问的站点设置的Cookie,如果Cookie 存在的话,会把这些信息放在 NSHTTPCookieStorage容器中共享,当你下次再访问这个站点时,NSURLRequest会拿着上次保存下来了的Cookie继续去请求。
所以UIWebView的Cookie管理很简单,一般不需要我们手动操作Cookie,全部Cookie都会被[NSHTTPCookieStorage sharedHTTPCookieStorage]这个单例管理,而且UIWebView会自动同步CookieStorage中的Cookie,所以只要我们在Native端,正常登陆退出,h5在适当时候刷新,就可以正确的维持登录状态,不需要做多余的操作。
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);
}
}
又到了知识小课堂的时间。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;
NSHTTPCookieStorage类采用单例的设计模式,其中管理着所有HTTP请求的Cookie信息,更改cookie的接收政策将会影响当前所有正在使用cookie的app。
//所有Cookie数据数组 其中存放NSHTTPCookie对象
@property (nullable , readonly, copy) NSArray<NSHTTPCookie *> *cookies;
@property NSHTTPCookieAcceptPolicy cookieAcceptPolicy;//Cookie数据的接收协议
//获取单例对象
+ (NSHTTPCookieStorage *)sharedHTTPCookieStorage;
//手动设置一条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
// 存放和获取一个task任务所对应的cookie
- (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管理有关:
//Cookie数据的接收协议改变时发送的通知
FOUNDATION_EXPORT NSString * const NSHTTPCookieManagerAcceptPolicyChangedNotification;
//管理的Cookie数据发生变化时发送的通知
FOUNDATION_EXPORT NSString * const NSHTTPCookieManagerCookiesChangedNotification;
看看运行的结果打印出来的Cookie是怎样的...


需要注意的是
Cookie在在iOS中不会多应用共享,但是会在不同进程之间保持同步,Session Cookie(一个isSessionOnly方法返回YES的Cookie)只能在单一进程中使用。至于其他属性,在之前介绍NSHTTPCookie有提到。
3、设置UIWebView的Cookies
首先我们需要实现一个设置新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];
取出刚设置的新cookie设置请求头:
- (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];
}
运行APP验证下我们的Demo效果。创建了新cookie,设置了其属性后存储下来。

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

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

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

4、获取WKWebView的Cookies
接下来的过程可能有点绕,最初我也整懵了......大家要做好心理准备。不知道苹果为什么给WKWebView设置了这么一个坑?原谅我才疏学浅不懂原因,要不是看了大家的文章,都不知道还有这种鬼问题。
UIWebView 的Cookie是通过 NSHTTPCookieStorage统一管理,服务器返回时写入,发起请求时读取,Web 和 Native 通过该对象能共享 Cookie。
说起WKWebview代替UIWebview带来的好处你可以举出一堆堆的例子,但说到 WKWebview的问题,除了WKWebview视图尺寸问题,默认跳转被屏蔽,需要手动交互之外,你绕不过的就是WKWebview cookie 和 NSHTTPCookieStorage cookie不共享的问题。如何将 NSHTTPCookieStorage 同步给WKWebview,大概要处理很多种情况:
- 初次加载页面时,同步
cookie到WKWebview - 如果
response里有set-cookie还需要缓存这些cookie - 如果是新页面跳转,还需要处理
cookie传递的问题 - 处理
ajax请求时,需要的cookie
那么我们不禁好奇为什么NSHTTPCookieStorage 和 WKWebview 没有同步呢?首先来看看WKWebview cookie是怎么存储的?
session 级别的 cookie保存在 WKProcessPool里,每个 WKWebview 都可以关联一个 WKProcessPool的实例,如果需要在整个 App 生命周期里访问 h5 保留 h5 里的登录状态,可以使用 WKProcessPool的单例来共享登录状态。解释下,WKProcessPool 是个没有属性和方法的对象,唯一的作用就是标识是不是需要新的 session 级别的管理对象,一个实例代表一个对象。
未过期的 cookie。有效期内的 cookie 被持久化存储在 NSLibraryDirectory 目录下的 Cookies/文件夹。com.xiejiapei.NSURLProtocolDemo.binarycookies是 NSHTTPCookieStorage 文件对象。cookie.binarycookies则是WKWebview的实例化对象。这也是为什么WKWebview和 NSHTTPCookieStorage 没有同步的原因——因为被保存在不同的文件当中。

为了验证,你可以打开这两者文件进行查看:当然两个文件都是 binary file,直接用文本浏览器打开是看不到,有一个python写的脚本BinaryCookieReader可以读出来,我不怎么懂python,就不展开了。
明白了存储方式,让我们来思考🤔下WKWebview Cookie究竟是如何工作的?
系统默认方式
当 webview loadRequest 或者 302重定向 或者在 webview 加载完毕触发了 ajax请求时,WKWebview所需的 Cookie 会去 Cookie.binarycookies 里读取本域名下的 Cookie,加上WKProcessPool持有的Cookie 一起作为request 头里的Cookie 数据。
这种方式的问题是NSHTTPCookieStorage 的 Cookie 根本没有共享给 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];
}


// 这是上面👆那一串完整的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
需要注意的是,并非说系统的NSHTTPCookieStorage和WKWebView中所有Cookie都无法自动同步,两个存储文件完全各自为政。WKWebView加载网页得到的Cookie会同步到NSHTTPCookieStorage中(优秀🥳)。但是WKWebView加载请求时,不会同步NSHTTPCookieStorage中已有的Cookie(最为致命😒)。既然发现了问题,接下来就要大刀阔斧地干了! (凶恶嘴脸😎)
二、解决WKWebView中的Cookies问题
1、解决首次加载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信息。


2、解决跳转新页面时Cookie带不过去问题
这里的问题是当你点击页面上的某个链接,跳转到新的页面,Cookie又丢了......好弱智啊......怎么解决呢?新建了一个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];
}
在.h文件里声明了fixNewRequestCookieWithRequest方法
/**
解决新的跳转 Cookie 丢失问题
@param originalRequest 拦截的请求
@return 带上 Cookie 的新请求
*/
- (NSURLRequest *)fixNewRequestCookieWithRequest:(NSURLRequest *)originalRequest;
在.m文件中来实现该方法,首先需要注意的是如果navigationAction.request是NSURLRequest,不可变,那不就添加不了Cookie了,但我们不能因为这个问题不允许跳转,所以我们这里需要让它可变。其中因为传入是NSURLRequest,但是其实际类型为NSMutableURLRequest,我们就可以根据里氏替换原则对其进行运行时强制转化为子类。而当其为NSURLRequest,只需要进行可变拷贝即可,为深拷贝。里氏替换原则指的是父类可以被子类无缝替换,且原有功能不受影响,例如KVO实现原理,调用addObserver方法,系统在动态运行时候为我们创建一个子类,我们虽然感受到的是使用原有的父类,实际上是子类。
NSMutableURLRequest *fixedRequest;
if ([originalRequest isKindOfClass:[NSMutableURLRequest class]])
{
fixedRequest = (NSMutableURLRequest *)originalRequest;
}
else
{
// 只需要进行可变拷贝即可
fixedRequest = originalRequest.mutableCopy;
}
取出解决问题一时候的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的:

问题三:解决后续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抓包发现点击一个链接时,Request的header中多了Set-Cookie字段,其实Cookie已经丢了。
解决方案那就是把自己需要的Cookie主动保存起来,每次调用[NSHTTPCookieStorage sharedHTTPCookieStorage].cookies方法时,保证返回的数组中有自己需要的Cookie。下面上代码,用了runtime的Method 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,就需要我们创建一个,创建新Cookie的fetchAccessTokenCookie方法如下:
// 创建新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

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

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

拓展:Cookie 污染问题
原因:如果我们自己设置了 allHTTPHeaderFields,则系统不会使用 the cookie manager by default。
解决方案:所以我们的方案是在页面加载过程中不去设置 allHTTPHeaderFields,全部使用默认 Cookie mananger管理,这样就不会有 Cookie 污染也不会有302 Cookie丢失的问题了。
唯一的问题:如何将 NSHTTPCookieStorage 的 Cookie共享给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;
}
}
同时记得删掉原来对 webview的Cookie 的所有处理的代码。
处理至此,大功告成,这样的后续请求, WKWebview都用自身所有的Cookie 和NSHTTPCookieStorage的 Cookie,这样既达到了 Cookie 共享的目的,WKWebview和 NSHTTPCookieStorage 的Cookie也做了个隔离。
这个方法,我看得懵懵懂懂,大家想要深入研究的话,在这个开源项目 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上,欢迎下载。
IOSAdvancedDemo