原创:知识进阶型文章
无私奉献,为国为民,创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的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 *)properties;
+ (nullable NSHTTPCookie *)cookieWithProperties:(NSDictionary *)properties;
//返回Cookie数据中可用于添加HTTP头字段的字典
+ (NSDictionary *)requestHeaderFieldsWithCookies:(NSArray *)cookies;
//从指定的响应头和URL地址中解析出Cookie数据
+ (NSArray *)cookiesWithResponseHeaderFields:(NSDictionary *)headerFields forURL:(NSURL *)URL;
//Cookie数据中的属性字典
@property (nullable, readonly, copy) NSDictionary *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 *portList;
NSHTTPCookieStorage
类采用单例的设计模式,其中管理着所有HTTP
请求的Cookie
信息,更改cookie
的接收政策将会影响当前所有正在使用cookie
的app
。
//所有Cookie数据数组 其中存放NSHTTPCookie对象
@property (nullable , readonly, copy) NSArray *cookies;
@property NSHTTPCookieAcceptPolicy cookieAcceptPolicy;//Cookie数据的接收协议
//获取单例对象
+ (NSHTTPCookieStorage *)sharedHTTPCookieStorage;
//手动设置一条Cookie数据
- (void)setCookie:(NSHTTPCookie *)cookie;
//删除某条Cookie信息
- (void)deleteCookie:(NSHTTPCookie *)cookie;
//获取某个特定URL的所有Cookie数据
- (nullable NSArray *)cookiesForURL:(NSURL *)URL;
//删除某个时间后的所有Cookie信息 iOS8后可用
- (void)removeCookiesSinceDate:(NSDate *)date NS_AVAILABLE(10_10, 8_0);
//为某个特定的URL设置Cookie
- (void)setCookies:(NSArray *)cookies forURL:(nullable NSURL *)URL mainDocumentURL:(nullable NSURL *)mainDocumentURL
// 存放和获取一个task任务所对应的cookie
- (void)storeCookies:(NSArray *)cookies forTask:(NSURLSessionTask *)task NS_AVAILABLE(10_10, 8_0);
- (void)getCookiesForTask:(NSURLSessionTask *)task completionHandler:(void (^) (NSArray * _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
,声明会实现
委托,实现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
框架,接着实现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_cookies
Get方法:
// 加载
+ (void)load
{
class_methodSwizzling(self, @selector(cookies), @selector(custom_cookies));
}
c、于是我们需要实现一下这个自定义的Get方法custom_cookies
:
// 自定义cookies
- (NSArray *)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 *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
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
#import "GCDMethods.h"
@interface UWWkWebViewCookieManager ()
@end
@implementation UWWkWebViewCookieManager
+ (void)load
{
if (@available(iOS 11.0, *)) {
[[[WKWebsiteDataStore defaultDataStore] httpCookieStore] addObserver:(id)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 * _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 * _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
推荐Demo
iOS中UIWebView与WKWebView、JavaScript与OC交互、Cookie管理看我就够
参考文献
- 这才是 WKWebview Cookie 管理的正确方式
- iOS中UIWebView与WKWebView、JavaScript与OC交互、Cookie管理看我就够