原創(chuàng):知識進(jìn)階型文章
無私奉獻(xiàn)虐秦,為國為民平酿,創(chuàng)作不易,請珍惜悦陋,之后會持續(xù)更新染服,不斷完善
個人比較喜歡做筆記和寫總結(jié),畢竟好記性不如爛筆頭哈哈叨恨,這些文章記錄了我的IOS成長歷程柳刮,希望能與大家一起進(jìn)步
溫馨提示:由于簡書不支持目錄跳轉(zhuǎn),大家可通過command + F 輸入目錄標(biāo)題后迅速尋找到你所需要的內(nèi)容
目錄
- 一痒钝、設(shè)置Cookie
- 1秉颗、什么是Cookie
- 2、獲得UIWebView的Cookies
- 3送矩、設(shè)置UIWebView的Cookies
- 4蚕甥、獲取WKWebView的Cookies
- 二、解決WKWebView中的Cookies問題
- 1栋荸、解決首次加載Cookie帶不上問題
- 2菇怀、解決跳轉(zhuǎn)新頁面時Cookie帶不過去問題
- 問題三:解決后續(xù)Ajax請求(局部頁面更新請求)Cookie丟失問題
- 問題四:如果 response 里有 set-cookie 還需要緩存這些 cookie
- 拓展:Cookie 污染問題
- 更新:iOS 11后雙向同步cookie簡便方式
- Demo
- 參考文獻(xiàn)
一凭舶、設(shè)置Cookie
1、什么是Cookie
Cookie
是由服務(wù)器端生成爱沟,發(fā)送給User-Agent
(一般是瀏覽器或者客戶端)帅霜,瀏覽器會將Cookie
的key/value
保存到某個目錄下的文本文件內(nèi),下次請求同一網(wǎng)站地址時就發(fā)送該Cookie
給服務(wù)器呼伸。Cookie
必然會通過HTTP
的Respone
傳過來身冀,并且Cookie
在Respone
中的HTTP header
中。
為什么需要Cookie括享?
HTTP
是一種無狀態(tài)的協(xié)議搂根,客戶端與服務(wù)器建立連接并傳輸數(shù)據(jù),數(shù)據(jù)傳輸完成后铃辖,連接就會關(guān)閉剩愧。再次交互數(shù)據(jù)需要建立新的連接,因此娇斩,服務(wù)器無法從連接上跟蹤會話隙咸,也無法知道用戶上一次做了什么。這嚴(yán)重阻礙了基于Web應(yīng)用程序的交互成洗,也影響用戶的交互體驗(yàn)五督。如:在網(wǎng)絡(luò)有時候需要用戶登錄才進(jìn)一步操作,用戶輸入用戶名密碼登錄后瓶殃,瀏覽了幾個頁面充包,由于HTTP
的無狀態(tài)性,服務(wù)器并不知道用戶有沒有登錄遥椿。
Cookie
是解決HTTP
無狀態(tài)性的有效手段基矮,服務(wù)器可以設(shè)置或讀取Cookie
中所包含的信息。當(dāng)用戶登錄后冠场,服務(wù)器會發(fā)送包含登錄憑據(jù)的Cookie
到用戶瀏覽器客戶端家浇,而瀏覽器對該Cookie
進(jìn)行某種形式的存儲(內(nèi)存或硬盤)。用戶再次訪問該網(wǎng)站時碴裙,瀏覽器會發(fā)送該Cookie
(Cookie
未到期時)到服務(wù)器钢悲,服務(wù)器對該憑據(jù)進(jìn)行驗(yàn)證,合法時使用戶不必輸入用戶名和密碼就可以直接登錄舔株。
實(shí)際項(xiàng)目中使用場景如:當(dāng)Native
端用戶是登錄狀態(tài)的莺琳,打開一個h5
頁面,h5
也要維持用戶的登錄狀態(tài)载慈。這個需求看似簡單惭等,如何實(shí)現(xiàn)呢?一般的解決方案是Native
保存登錄狀態(tài)的Cookie
办铡,在打開h5
頁面中辞做,把Cookie
添加上琳要,以此來維持登錄狀態(tài)。其實(shí)坑還是有很多的秤茅,比如用戶登錄或者退出了稚补,h5
頁面的登錄狀態(tài)也變了,需要刷新嫂伞,什么時候刷新孔厉?WKWebView中Cookie
丟失問題拯钻?
cookie的類型
Cookie
總是由用戶客戶端進(jìn)行保存的(一般是瀏覽器)帖努,按其存儲位置可分為:內(nèi)存式Cookie
(指在不設(shè)定它的生命周期expires
時的狀態(tài))和硬盤式Cookie
。內(nèi)存式Cookie
存儲在內(nèi)存中粪般,瀏覽器關(guān)閉后就會消失拼余,由于其存儲時間較短,因此也被稱為非持久Cookie
或會話Cookie
亩歹。硬盤式Cookie
保存在硬盤中匙监,其不會隨瀏覽器的關(guān)閉而消失,除非用戶手工清理或到了過期時間小作。由于硬盤式Cookie
存儲時間是長期的亭姥,因此也被稱為持久Cookie
。
cookie實(shí)現(xiàn)原理
cookie
定義了一些HTTP
請求頭和HTTP
響應(yīng)頭顾稀,通過這些HTTP
頭信息使服務(wù)器可以與客戶端進(jìn)行狀態(tài)交互达罗。客戶端請求服務(wù)器后静秆,如果服務(wù)器需要記錄用戶狀態(tài)粮揉,服務(wù)器會在響應(yīng)信息中包含一個Set-Cookie
的響應(yīng)頭,客戶端會根據(jù)這個響應(yīng)頭存儲Cookie
信息抚笔。再次請求服務(wù)器時扶认,客戶端會在請求信息中包含一個Cookie
請求頭,而服務(wù)器會根據(jù)這個請求頭進(jìn)行用戶身份殊橙、狀態(tài)等較驗(yàn)辐宾。
與session的區(qū)別
cookie
機(jī)制采用的是在客戶端保持狀態(tài)的方案,而session
機(jī)制采用的是在服務(wù)器端保持狀態(tài)的方案膨蛮。由于采用服務(wù)器端保持狀態(tài)的方案在客戶端也需要保存一個標(biāo)識螃概,所以session
機(jī)制也需要借助于cookie
機(jī)制來達(dá)到保存標(biāo)識的目的。
iOS中的Cookie
當(dāng)你訪問一個網(wǎng)站時鸽疾,NSURLRequest
都會幫你主動記錄下來你訪問的站點(diǎn)設(shè)置的Cookie
吊洼,如果Cookie
存在的話,會把這些信息放在 NSHTTPCookieStorage
容器中共享制肮,當(dāng)你下次再訪問這個站點(diǎn)時冒窍,NSURLRequest
會拿著上次保存下來了的Cookie
繼續(xù)去請求递沪。
所以UIWebView
的Cookie
管理很簡單,一般不需要我們手動操作Cookie
综液,全部Cookie
都會被[NSHTTPCookieStorage sharedHTTPCookieStorage]
這個單例管理款慨,而且UIWebView
會自動同步CookieStorage
中的Cookie
,所以只要我們在Native
端谬莹,正常登陸退出檩奠,h5
在適當(dāng)時候刷新,就可以正確的維持登錄狀態(tài)附帽,不需要做多余的操作埠戳。
1、獲得UIWebView的Cookies
實(shí)現(xiàn)webViewCookiesButton
的調(diào)用方法webViewCookies
:
- (void)webViewCookies
{
// 創(chuàng)建新的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
整胃。
//下面兩個方法用于對象的創(chuàng)建和初始化 都是通過字典進(jìn)行鍵值設(shè)置
- (nullable instancetype)initWithProperties:(NSDictionary<NSString *, id> *)properties;
+ (nullable NSHTTPCookie *)cookieWithProperties:(NSDictionary<NSString *, id> *)properties;
//返回Cookie數(shù)據(jù)中可用于添加HTTP頭字段的字典
+ (NSDictionary<NSString *, NSString *> *)requestHeaderFieldsWithCookies:(NSArray<NSHTTPCookie *> *)cookies;
//從指定的響應(yīng)頭和URL地址中解析出Cookie數(shù)據(jù)
+ (NSArray<NSHTTPCookie *> *)cookiesWithResponseHeaderFields:(NSDictionary<NSString *, NSString *> *)headerFields forURL:(NSURL *)URL;
//Cookie數(shù)據(jù)中的屬性字典
@property (nullable, readonly, copy) NSDictionary<NSString *, id> *properties;
//請求響應(yīng)的版本
@property (readonly) NSUInteger version;
//請求相應(yīng)的名稱
@property (readonly, copy) NSString *name;
//請求相應(yīng)的值
@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;
//是否只發(fā)送HTTP的服務(wù)
@property (readonly, getter=isHTTPOnly) BOOL HTTPOnly;
//響應(yīng)的文檔
@property (nullable, readonly, copy) NSString *comment;
//相應(yīng)的文檔URL
@property (nullable, readonly, copy) NSURL *commentURL;
//服務(wù)端口列表
@property (nullable, readonly, copy) NSArray<NSNumber *> *portList;
NSHTTPCookieStorage
類采用單例的設(shè)計(jì)模式,其中管理著所有HTTP
請求的Cookie
信息喳钟,更改cookie
的接收政策將會影響當(dāng)前所有正在使用cookie
的app
屁使。
//所有Cookie數(shù)據(jù)數(shù)組 其中存放NSHTTPCookie對象
@property (nullable , readonly, copy) NSArray<NSHTTPCookie *> *cookies;
@property NSHTTPCookieAcceptPolicy cookieAcceptPolicy;//Cookie數(shù)據(jù)的接收協(xié)議
//獲取單例對象
+ (NSHTTPCookieStorage *)sharedHTTPCookieStorage;
//手動設(shè)置一條Cookie數(shù)據(jù)
- (void)setCookie:(NSHTTPCookie *)cookie;
//刪除某條Cookie信息
- (void)deleteCookie:(NSHTTPCookie *)cookie;
//獲取某個特定URL的所有Cookie數(shù)據(jù)
- (nullable NSArray<NSHTTPCookie *> *)cookiesForURL:(NSURL *)URL;
//刪除某個時間后的所有Cookie信息 iOS8后可用
- (void)removeCookiesSinceDate:(NSDate *)date NS_AVAILABLE(10_10, 8_0);
//為某個特定的URL設(shè)置Cookie
- (void)setCookies:(NSArray<NSHTTPCookie *> *)cookies forURL:(nullable NSURL *)URL mainDocumentURL:(nullable NSURL *)mainDocumentURL
// 存放和獲取一個task任務(wù)所對應(yīng)的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);
系統(tǒng)下面的兩個通知與Cookie
管理有關(guān):
//Cookie數(shù)據(jù)的接收協(xié)議改變時發(fā)送的通知
FOUNDATION_EXPORT NSString * const NSHTTPCookieManagerAcceptPolicyChangedNotification;
//管理的Cookie數(shù)據(jù)發(fā)生變化時發(fā)送的通知
FOUNDATION_EXPORT NSString * const NSHTTPCookieManagerCookiesChangedNotification;
看看運(yùn)行的結(jié)果打印出來的Cookie
是怎樣的...
需要注意的是
Cookie
在在iOS中不會多應(yīng)用共享,但是會在不同進(jìn)程之間保持同步奔则,Session Cookie
(一個isSessionOnly
方法返回YES
的Cookie
)只能在單一進(jìn)程中使用蛮寂。至于其他屬性,在之前介紹NSHTTPCookie
有提到易茬。
3酬蹋、設(shè)置UIWebView的Cookies
首先我們需要實(shí)現(xiàn)一個設(shè)置新Cookies
的方法來對Cookies
的各項(xiàng)屬性值進(jìn)行設(shè)置。
- (void)setCookieWithDomain:(NSString*)domainValue
sessionName:(NSString *)name
sessionValue:(NSString *)value
expiresDate:(NSDate *)date
其中對各項(xiàng)屬性值進(jìn)行設(shè)置的部分如下:
// 創(chuàng)建字典存儲cookie的屬性值
NSMutableDictionary *cookieProperties = [NSMutableDictionary dictionary];
// 設(shè)置cookie名
[cookieProperties setObject:name forKey:NSHTTPCookieName];
// 設(shè)置cookie值
[cookieProperties setObject:value forKey:NSHTTPCookieValue];
// 設(shè)置cookie域名
NSURL *url = [NSURL URLWithString:domainValue];
NSString *domain = [url host];
[cookieProperties setObject:domain forKey:NSHTTPCookieDomain];
// 設(shè)置cookie路徑 一般寫"/"
[cookieProperties setObject:@"/" forKey:NSHTTPCookiePath];
// 設(shè)置cookie版本, 默認(rèn)寫0
[cookieProperties setObject:@"0" forKey:NSHTTPCookieVersion];
設(shè)置cookie
過期時間:
if (date)
{
[cookieProperties setObject:date forKey:NSHTTPCookieExpires];
}
else
{
// 推遲一年
NSDate *date = [NSDate dateWithTimeIntervalSince1970:([[NSDate date] timeIntervalSince1970] + 365*24*3600)];
[cookieProperties setObject:date forKey:NSHTTPCookieExpires];
}
因?yàn)槭謩釉O(shè)置的Cookie
不會自動持久化到沙盒疾呻,所以需要我們自己來實(shí)現(xiàn)除嘹。設(shè)置cookie
的屬性值到本地磁盤,因?yàn)槭謩釉O(shè)置的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];
補(bǔ)充一點(diǎn),如果我們想清除某一個url
緩存璃岳,可以這樣來做:
[NSURLCache sharedURLCache] removeCachedResponseForRequest:[NSURLRequest requestWithURL:url];
取出剛設(shè)置的新cookie
設(shè)置請求頭:
- (void)setWebViewCookies
{
// 設(shè)置新Cookies
[self setCookieWithDomain:@"http://www.baidu.com" sessionName:@"xiejiapei_token_UIWebView" sessionValue:@"55555555" expiresDate:nil];
// 取出剛設(shè)置的新cookie
NSArray *cookiesArray = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
NSDictionary *headerCookieDict = [NSHTTPCookie requestHeaderFieldsWithCookies:cookiesArray];
// 設(shè)置請求頭
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://www.baidu.com"]];
request.allHTTPHeaderFields = headerCookieDict;
[self.webView loadRequest:request];
}
運(yùn)行APP驗(yàn)證下我們的Demo效果年缎。創(chuàng)建了新cookie
,設(shè)置了其屬性后存儲下來铃慷。
取出剛設(shè)置的新cookie
单芜,將其設(shè)置為請求頭
實(shí)際運(yùn)行后,通過Charles
捕獲網(wǎng)絡(luò)請求犁柜,在狀態(tài)碼為302的請求的Content
中我們看到確實(shí)存儲了剛才自己設(shè)置的cookie
洲鸠,并且在本地沙盒Preferences
中,打開.plist
文件,cookie
也成功保存到了本地
點(diǎn)擊webViewCookiesButton
后扒腕,相應(yīng)的控制臺也的確打印出了我們設(shè)置的cookie
4绢淀、獲取WKWebView的Cookies
接下來的過程可能有點(diǎn)繞,最初我也整懵了......大家要做好心理準(zhǔn)備瘾腰。不知道蘋果為什么給WKWebView
設(shè)置了這么一個坑皆的?原諒我才疏學(xué)淺不懂原因,要不是看了大家的文章蹋盆,都不知道還有這種鬼問題费薄。
UIWebView
的Cookie
是通過 NSHTTPCookieStorage
統(tǒng)一管理,服務(wù)器返回時寫入栖雾,發(fā)起請求時讀取楞抡,Web
和 Native
通過該對象能共享 Cookie
。
說起WKWebview
代替UIWebview
帶來的好處你可以舉出一堆堆的例子岩灭,但說到 WKWebview
的問題拌倍,除了WKWebview
視圖尺寸問題赂鲤,默認(rèn)跳轉(zhuǎn)被屏蔽噪径,需要手動交互之外,你繞不過的就是WKWebview cookie
和 NSHTTPCookieStorage cookie
不共享的問題数初。如何將 NSHTTPCookieStorage
同步給WKWebview
找爱,大概要處理很多種情況:
- 初次加載頁面時,同步
cookie
到WKWebview
- 如果
response
里有set-cookie
還需要緩存這些cookie
- 如果是新頁面跳轉(zhuǎn)泡孩,還需要處理
cookie
傳遞的問題 - 處理
ajax
請求時车摄,需要的cookie
那么我們不禁好奇為什么NSHTTPCookieStorage
和 WKWebview
沒有同步呢?首先來看看WKWebview cookie
是怎么存儲的?
session
級別的 cookie
保存在 WKProcessPool
里仑鸥,每個 WKWebview
都可以關(guān)聯(lián)一個 WKProcessPool
的實(shí)例吮播,如果需要在整個 App 生命周期里訪問 h5 保留 h5 里的登錄狀態(tài),可以使用 WKProcessPool
的單例來共享登錄狀態(tài)眼俊。解釋下意狠,WKProcessPool
是個沒有屬性和方法的對象,唯一的作用就是標(biāo)識是不是需要新的 session
級別的管理對象疮胖,一個實(shí)例代表一個對象环戈。
未過期的 cookie
。有效期內(nèi)的 cookie
被持久化存儲在 NSLibraryDirectory
目錄下的 Cookies/
文件夾澎灸。com.xiejiapei.NSURLProtocolDemo.binarycookies
是 NSHTTPCookieStorage
文件對象院塞。cookie.binarycookies
則是WKWebview
的實(shí)例化對象。這也是為什么WKWebview
和 NSHTTPCookieStorage
沒有同步的原因——因?yàn)楸槐4嬖诓煌奈募?dāng)中性昭。
為了驗(yàn)證拦止,你可以打開這兩者文件進(jìn)行查看:當(dāng)然兩個文件都是 binary file
,直接用文本瀏覽器打開是看不到糜颠,有一個python
寫的腳本BinaryCookieReader可以讀出來汹族,我不怎么懂python
艺玲,就不展開了。
明白了存儲方式鞠抑,讓我們來思考??下WKWebview Cookie
究竟是如何工作的饭聚?
系統(tǒng)默認(rèn)方式
當(dāng) webview loadRequest
或者 302重定向
或者在 webview
加載完畢觸發(fā)了 ajax
請求時,WKWebview
所需的 Cookie
會去 Cookie.binarycookies
里讀取本域名下的 Cookie
搁拙,加上WKProcessPool
持有的Cookie
一起作為request
頭里的Cookie
數(shù)據(jù)秒梳。
這種方式的問題是NSHTTPCookieStorage
的 Cookie
根本沒有共享給 WKWebview
,沒有涉及到session
暫不考慮WKProcessPool
箕速,因此導(dǎo)致request
頭里的Cookie
數(shù)據(jù)為空酪碘,即allHTTPHeaderFields
為空,這就是萬惡之源啊啊啊啊??~讓我們實(shí)際驗(yàn)證下控制臺輸出結(jié)果盐茎。
引入#import <WebKit/WebKit.h>
兴垦,聲明會實(shí)現(xiàn)<WKNavigationDelegate>
委托,實(shí)現(xiàn)wkWebViewCookiesButton
的調(diào)用方法wkWebViewCookies
- (void)wkWebViewCookies
{
// 創(chuàng)建新的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信息字柠,可以看到?jīng)]有我們自己設(shè)置的那部分信息
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
需要注意的是探越,并非說系統(tǒng)的NSHTTPCookieStorage
和WKWebView
中所有Cookie
都無法自動同步,兩個存儲文件完全各自為政窑业。WKWebView
加載網(wǎng)頁得到的Cookie
會同步到NSHTTPCookieStorage
中(優(yōu)秀??)钦幔。但是WKWebView
加載請求時,不會同步NSHTTPCookieStorage
中已有的Cookie
(最為致命??)常柄。既然發(fā)現(xiàn)了問題鲤氢,接下來就要大刀闊斧地干了! (兇惡嘴臉??)
二、解決WKWebView中的Cookies問題
1西潘、解決首次加載Cookie帶不上問題
這個比較簡單卷玉,Cookies
數(shù)組轉(zhuǎn)換為requestHeaderFields
,再將其設(shè)置為請求頭即可喷市,這樣相种,只要你保證sharedHTTPCookieStorage
中你的Cookie
存在,首次訪問一個頁面东抹,就不會有問題蚂子。
- (void)wkWebViewCookies
{
// 創(chuàng)建新的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數(shù)組轉(zhuǎn)換為requestHeaderFields
NSDictionary *requestHeaderFields = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
// 設(shè)置請求頭
request.allHTTPHeaderFields = requestHeaderFields;
NSLog(@"request.allHTTPHeaderFields: %@",request.allHTTPHeaderFields);
[self.wkWebView loadRequest:request];
}
看下運(yùn)行效果,發(fā)現(xiàn)我們成功將其設(shè)置為了請求頭缭黔,這樣request.allHTTPHeaderFields
就不為空了,并且Charles
也捕獲到了該Cookie
信息馏谨。
2、解決跳轉(zhuǎn)新頁面時Cookie帶不過去問題
這里的問題是當(dāng)你點(diǎn)擊頁面上的某個鏈接喇伯,跳轉(zhuǎn)到新的頁面买喧,Cookie
又丟了......好弱智啊......怎么解決呢淤毛?新建了一個WKCookieManager
工具類今缚,用更安全的方式設(shè)置了一個單例來方便調(diào)用之后的方法。
+ (instancetype)shareManager
{
// 靜態(tài)局部變量
static WKCookieManager *_instance;
// 通過dispatch_ once方式確保instance在多線程環(huán)境下只被創(chuàng)建一次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 創(chuàng)建實(shí)例
// super: 不能使用self,否則重寫的allocWithZone第一次初始化的時候 會循環(huán)調(diào)用instance
_instance = [[super allocWithZone:NULL] init];
});
return _instance;
}
// 重寫方法[必不可少]
// 規(guī)避逃脫sharedInstance再去創(chuàng)建其他對象低淡,當(dāng)alloc的時候只能返回單例
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
return [self shareManager];
}
在.h
文件里聲明了fixNewRequestCookieWithRequest
方法
/**
解決新的跳轉(zhuǎn) Cookie 丟失問題
@param originalRequest 攔截的請求
@return 帶上 Cookie 的新請求
*/
- (NSURLRequest *)fixNewRequestCookieWithRequest:(NSURLRequest *)originalRequest;
在.m
文件中來實(shí)現(xiàn)該方法姓言,首先需要注意的是如果navigationAction.request
是NSURLRequest
,不可變蔗蹋,那不就添加不了Cookie
了何荚,但我們不能因?yàn)檫@個問題不允許跳轉(zhuǎn),所以我們這里需要讓它可變纸颜。其中因?yàn)閭魅胧?code>NSURLRequest兽泣,但是其實(shí)際類型為NSMutableURLRequest
绎橘,我們就可以根據(jù)里氏替換原則對其進(jìn)行運(yùn)行時強(qiáng)制轉(zhuǎn)化為子類胁孙。而當(dāng)其為NSURLRequest
,只需要進(jìn)行可變拷貝即可称鳞,為深拷貝涮较。里氏替換原則指的是父類可以被子類無縫替換,且原有功能不受影響冈止,例如KVO
實(shí)現(xiàn)原理狂票,調(diào)用addObserver
方法,系統(tǒng)在動態(tài)運(yùn)行時候?yàn)槲覀儎?chuàng)建一個子類熙暴,我們雖然感受到的是使用原有的父類闺属,實(shí)際上是子類。
NSMutableURLRequest *fixedRequest;
if ([originalRequest isKindOfClass:[NSMutableURLRequest class]])
{
fixedRequest = (NSMutableURLRequest *)originalRequest;
}
else
{
// 只需要進(jìn)行可變拷貝即可
fixedRequest = originalRequest.mutableCopy;
}
取出解決問題一時候的NSHTTPCookieStorage
中的Cookie
周霉,并將其設(shè)置為fixedRequest.allHTTPHeaderFields
掂器,其實(shí)解決思路都一樣,就是它沒有那么就從保存下來的地方給它一個就好了俱箱。
// 關(guān)鍵步驟:防止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;
打斷點(diǎn)調(diào)試下国瓮,看是否能行,結(jié)果顯示是OK的:
問題三:解決后續(xù)Ajax請求(局部頁面更新請求)Cookie丟失問題
AJAX = Asynchronous JavaScript and XML(異步的 JavaScript 和 XML)。
AJAX 不是新的編程語言乃摹,而是一種使用現(xiàn)有標(biāo)準(zhǔn)的新方法禁漓。
AJAX 最大的優(yōu)點(diǎn)是在不重新加載整個頁面的情況下,可以與服務(wù)器交換數(shù)據(jù)并更新部分網(wǎng)頁內(nèi)容孵睬。
AJAX 不需要任何瀏覽器插件播揪,但需要用戶允許JavaScript在瀏覽器上執(zhí)行。
解決此問題的關(guān)鍵是注入的 JS 代碼塊熏兄。
a姜贡、在.h
文件里聲明了fixNewRequestCookieWithRequest
方法
/**
Ajax請求(局部頁面更新請求)Cookie 丟失問題
@return 注入的 JS 代碼塊
*/
- (WKUserScript *)futhureCookieScript;
b、在.m
文件中來實(shí)現(xiàn)該方法磷支,此處需要注意forMainFrameOnly
為NO谒撼,因?yàn)槲覀冃枰獙ookie注入到所有frames
// Ajax請求(局部頁面更新請求)Cookie 丟失問題
- (WKUserScript *)futhureCookieScript
{
// 只讀屬性,表示JS是否應(yīng)該注入到所有的frames中還是只有main frame
WKUserScript *cookieScript = [[WKUserScript alloc] initWithSource:[self cookieString] injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
return cookieScript;
}
相應(yīng)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
的擴(kuò)展方法:
#import "NSHTTPCookie+Util.h"
@implementation NSHTTPCookie (Util)
// 將cookie格式化為string的擴(kuò)展方法
- (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
中調(diào)用我們剛才實(shí)現(xiàn)的方法廓潜,此時創(chuàng)建新的WKWebView需要采用configuration
的初始化方式,為了向contoller
中注入腳本
// 創(chuàng)建新的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
存在移盆,后續(xù)Ajax
請求就不會有問題悼院。
問題四:如果 response 里有 set-cookie 還需要緩存這些 cookie
保證sharedHTTPCookieStorage
中你的Cookie
存在。怎么保證呢咒循?由于WKWebView
加載網(wǎng)頁得到的Cookie
會同步到NSHTTPCookieStorage
中的特點(diǎn)据途,有時候你強(qiáng)行添加的Cookie
會在同步過程中丟失。Charles
抓包發(fā)現(xiàn)點(diǎn)擊一個鏈接時叙甸,Request
的header
中多了Set-Cookie
字段颖医,其實(shí)Cookie
已經(jīng)丟了。
解決方案那就是把自己需要的Cookie
主動保存起來裆蒸,每次調(diào)用[NSHTTPCookieStorage sharedHTTPCookieStorage].cookies
方法時熔萧,保證返回的數(shù)組中有自己需要的Cookie
。下面上代碼僚祷,用了runtime
的Method Swizzling
佛致。
a、創(chuàng)建NSHTTPCookieStorage (CookieUtil)
擴(kuò)展方法文件辙谜,引入運(yùn)行時#import <objc/runtime.h>
框架俺榆,接著實(shí)現(xiàn)class_methodSwizzling
替換方法:
/**
* 方法替換。Method Swizzling技術(shù)筷弦。使類中的方法實(shí)現(xiàn)和自己的方法實(shí)現(xiàn)互換肋演,達(dá)到替換默認(rèn)抑诸,且還可以調(diào)用默認(rèn)方法的目的。
*
* @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);
// 如果可以在原有類中添加方法爹殊,說明原有的類并沒有實(shí)現(xiàn)蜕乡,可能是繼承自父類的方法。
// 那么层玲,我們添加一個方法反症,方法名為原方法名,實(shí)現(xiàn)為我們自己的實(shí)現(xiàn)润绵。之后再將自己的方法替換成原始的實(shí)現(xiàn)。
BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
//這么做胞谈,避免了替換方法時尘盼,由于本class中沒有實(shí)現(xiàn),從而替換了父類的方法烦绳。造成不可預(yù)知的錯誤卿捎。
if (didAddMethod)
{
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}
// 如果類中已經(jīng)實(shí)現(xiàn)了這個原始方法,那么就與我們的方法互換一下實(shí)現(xiàn)即可径密。
else
{
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
b午阵、接著需要在load
方法中調(diào)用我們的替換方法,將cookies
的GET方法替換為我們自定義的custom_cookies
Get方法:
// 加載
+ (void)load
{
class_methodSwizzling(self, @selector(cookies), @selector(custom_cookies));
}
c享扔、于是我們需要實(shí)現(xiàn)一下這個自定義的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為創(chuàng)建新Cookie的方法
NSHTTPCookie *cookie = [self fetchAccessTokenCookie];
[[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
// 添加到返回?cái)?shù)組中
NSMutableArray *mutableCookies = cookies.mutableCopy;
[mutableCookies addObject:cookie];
cookies = mutableCookies.copy;
}
return cookies;
}
d、如果NSHTTPCookieStorage
沒有我們想要的Cookie
伪很,就需要我們創(chuàng)建一個戚啥,創(chuàng)建新Cookie
的fetchAccessTokenCookie
方法如下:
// 創(chuàng)建新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
锉试,實(shí)現(xiàn)該方法后,在viewDidLoad
中調(diào)用
// 在合適的時候(如登錄成功)保存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;
}
}
}
看看運(yùn)行結(jié)果如何览濒?
運(yùn)行后首先會進(jìn)入方法交換方法class_methodSwizzling
進(jìn)入HTTPCookieViewController
頁面后馬上會進(jìn)入saveCookie
方法呆盖,由于NSArray *allCookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
調(diào)用了cookies
的Get方法,所以又立刻進(jìn)入到custom_cookies
中贷笛,第一次因?yàn)椴淮嬖谧远xCookies
需要進(jìn)行創(chuàng)造并存儲应又,所以mutableCookies
擁有兩個與元素,而cookie
卻擁有一個乏苦。
最后又重新進(jìn)入到saveCookie
方法,將以前保存的本地Cookie
和我們剛剛新設(shè)置的custom_cookies
的值進(jìn)行比較盆繁,我第一次設(shè)置的是linning
,第二次設(shè)置為xiejiapei
冕碟,因?yàn)閮纱尾幌嗟劝菜拢暂敵?code>cookies的值更新了我衬。
拓展:Cookie 污染問題
原因:如果我們自己設(shè)置了 allHTTPHeaderFields
,則系統(tǒng)不會使用 the cookie manager by default
破加。
解決方案:所以我們的方案是在頁面加載過程中不去設(shè)置 allHTTPHeaderFields
,全部使用默認(rèn) Cookie mananger
管理锭环,這樣就不會有 Cookie
污染也不會有302 Cookie
丟失的問題了辅辩。
唯一的問題:如何將 NSHTTPCookieStorage
的 Cookie
共享給WKWebview
。
`
實(shí)踐過程如下:
在首次加載 url
時撩鹿,檢查是否已經(jīng)同步過 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
{
// 設(shè)置加載頁面完畢后,里面的后續(xù)請求锈颗,如 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
和設(shè)置標(biāo)記位覆醇。
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];
// 加載真正的頁面;此時已經(jīng)有 App 的 cookie 存在了。
[webView removeFromSuperview];
[self loadWebPage];
return;
}
}
同時記得刪掉原來對 webview
的Cookie
的所有處理的代碼凿试。
處理至此,大功告成板甘,這樣的后續(xù)請求, WKWebview
都用自身所有的Cookie
和NSHTTPCookieStorage
的 Cookie
在跳,這樣既達(dá)到了 Cookie
共享的目的猫妙,WKWebview
和 NSHTTPCookieStorage
的Cookie
也做了個隔離割坠。
這個方法彼哼,我看得懵懵懂懂,大家想要深入研究的話象浑,在這個開源項(xiàng)目 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
推薦Demo
iOS中UIWebView與WKWebView、JavaScript與OC交互澳骤、Cookie管理看我就夠