IOS进阶:(网络篇)HTTPCookie
原创:知识进阶型文章
无私奉献,为国为民,创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容
目录
- Cookie
- 什么是Cookie
- 为什么需要Cookie?
- cookie的类型
- cookie实现原理
- 与session的区别
- iOS中的Cookie
- Demo实战
- 获得UIWebView的Cookies
- 设置UIWebView的Cookies
- 获取WKWebView的Cookies
- 问题一:解决首次加载Cookie带不上问题
- 问题二:解决跳转新页面时Cookie带不过去问题
- 问题三:解决后续Ajax请求(局部页面更新请求)Cookie丢失问题
- 问题四:如果 response 里有 set-cookie 还需要缓存这些 cookie
- 拓展:Cookie 污染问题
- 更新:iOS 11后双向同步cookie简便方式
- Demo
- 参考文献
一、Cookie
百度首页Cookie1、什么是Cookie
Cookie
是由服务器端生成,发送给User-Agent
(一般是浏览器或者客户端),浏览器会将Cookie的key/value
保存到某个目录下的文本文件内,下次请求同一网站地址时就发送该Cookie
给服务器。Cookie
必然会通过HTTP
的Respone
传过来,并且Cookie
在Respone
中的HTTP header
中。
2、为什么需要Cookie?
HTTP
是一种无状态的协议,客户端与服务器建立连接并传输数据,数据传输完成后,连接就会关闭。再次交互数据需要建立新的连接,因此,服务器无法从连接上跟踪会话,也无法知道用户上一次做了什么。这严重阻碍了基于Web应用程序的交互,也影响用户的交互体验。如:在网络有时候需要用户登录才进一步操作,用户输入用户名密码登录后,浏览了几个页面,由于HTTP
的无状态性,服务器并不知道用户有没有登录。
Cookie
是解决HTTP
无状态性的有效手段,服务器可以设置或读取Cookie
中所包含的信息。当用户登录后,服务器会发送包含登录凭据的Cookie
到用户浏览器客户端,而浏览器对该Cookie
进行某种形式的存储(内存或硬盘)。用户再次访问该网站时,浏览器会发送该Cookie
(Cookie
未到期时)到服务器,服务器对该凭据进行验证,合法时使用户不必输入用户名和密码就可以直接登录。
实际项目中使用场景如:当Native
端用户是登录状态的,打开一个h5
页面,h5
也要维持用户的登录状态。这个需求看似简单,如何实现呢?一般的解决方案是Native
保存登录状态的Cookie
,在打开h5
页面中,把Cookie
添加上,以此来维持登录状态。其实坑还是有很多的,比如用户登录或者退出了,h5
页面的登录状态也变了,需要刷新,什么时候刷新?WKWebView中Cookie
丢失问题?
3、cookie的类型
Cookie
总时由用户客户端进行保存的(一般是浏览器),按其存储位置可分为:内存式Cookie
(cookie
是指在不设定它的生命周期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);
}
}
又到了知识小课堂的时间
NSHTTPCookie
:NSHTTPCookie
对象代表一个HTTP cookie
。 这是一个不可改变的对象,从一个包含cookie
的属性的字典初始化,这个类可以用来手动创建cookie
的Properties
。
//下面两个方法用于对象的创建和初始化 都是通过字典进行键值设置
- (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
:NSHTTPCookieStorage
类采用单例的设计模式,其中管理着所有HTTP
请求的Cookie
信息,更改cookie
的接收政策将会影响当前所有正在使用cookie
的app
。
//获取单例对象
+ (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
是怎样的...
Cookie结构.png
需要注意的是
Cookie
在在iOS中不会多应用共享,但是会在不同进程之间保持同步,Session Cookie
(一个isSessionOnly
方法返回YES
的Cookie
)只能在单一进程中使用。至于其他属性,在之前介绍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、实现setWebViewCookiesButton
的setWebViewCookies
方法
- (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
,将其设置为请求头
实际运行后,通过Charles
捕获网络请求,在状态码为302的请求的Content
中我们看到确实存储了刚才自己设置的cookie
,并且在本地沙盒Preferences
中,打开.plist
文件,cookie
也成功保存到了本地
点击webViewCookiesButton
后,相应的控制台也的确打印出了我们设置的cookie
3、获取WKWebView的Cookies
接下来的过程可能有点绕,最初我也更整懵了......大家要做好心理准备。不知道苹果为什么给WKWebView
设置了这么一个坑?原谅我才疏学浅不懂原因,要不是看了大家的文章,都不知道还有这种鬼问题。
UIWebView
的Cookie
是通过 NSHTTPCookieStorage
统一管理,服务器返回时写入,发起请求时读取,Web
和 Native
通过该对象能共享 Cookie
。
说起WKWebview
代替UIWebview
带来的好处你可以举出一堆堆的例子,但说到 WKWebview
的问题,除了WKWebview
视图尺寸问题,默认跳转被屏蔽,需要手动交互之外,你绕不过的就是WKWebview cookie
和 NSHTTPCookieStorage cookie
不共享的问题。
如何将 NSHTTPCookieStorage
同步给WKWebview
,大概要处理很多种情况:
1、初次加载页面时,同步 cookie 到 WKWebview
2、处理 ajax 请求时,需要的 cookie
3、如果 response 里有 set-cookie 还需要缓存这些 cookie
4、如果是 302新页面跳转 还需要处理 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
https://gist.github.com/sh1n0b1/4bb8b737370bfe5f5ab8。可以读出来,我不怎么懂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];
}
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
⚠️ :需要注意的是,并非说系统的NSHTTPCookieStorage
和WKWebView
中所有Cookie
都无法自动同步,两个存储文件完全各自为政。
-
WKWebView
加载网页得到的Cookie
会同步到NSHTTPCookieStorage
中。(优秀🥳) - 但是
WKWebView
加载请求时,不会同步NSHTTPCookieStorage
中已有的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
信息。
Charles也捕获到了该Cookie信息
问题二:解决跳转新页面时Cookie带不过去问题
心有余而力不足,真机调试又出幺蛾子了,折腾了十多分钟,证书这些配置的东西真烦人。以后有机会再补上效果图。
这里的问题是当你点击页面上的某个链接,跳转到新的页面,Cookie
又丢了......好弱智啊......怎么解决呢?
需要注意的地方,如果navigationAction.request
是NSURLRequest
,不可变,那不就添加不了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.request
是NSURLRequest
,不可变,那不就添加不了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
抓包发现点击一个链接时,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_cookies
Get方法:
// 加载
+ (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上,欢迎下载。
NetworkAdvancedDemo
参考文献
这才是 WKWebview Cookie 管理的正确方式
iOS中UIWebView与WKWebView、JavaScript与OC交互、Cookie管理看我就够