美文网首页
IOS进阶:(网络篇)HTTPCookie

IOS进阶:(网络篇)HTTPCookie

作者: 时光啊混蛋_97boy | 来源:发表于2020-10-28 09:41 被阅读0次

    原创:知识进阶型文章
    无私奉献,为国为民,创作不易,请珍惜,之后会持续更新,不断完善
    个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的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

    百度首页Cookie

    1、什么是Cookie

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

    2、为什么需要Cookie?

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

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

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

    3、cookie的类型

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

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

    4、cookie实现原理

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

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

    5、与session的区别

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

    6、iOS中的Cookie

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

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

    二、Demo实战

    1、获得UIWebView的Cookies

    实现webViewCookiesButton的调用方法webViewCookies:

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

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

    //下面两个方法用于对象的创建和初始化 都是通过字典进行键值设置
    - (nullable instancetype)initWithProperties:(NSDictionary<NSString *, id> *)properties;
    
    + (nullable NSHTTPCookie *)cookieWithProperties:(NSDictionary<NSString *, id> *)properties;
    
    //返回Cookie数据中可用于添加HTTP头字段的字典
    + (NSDictionary<NSString *, NSString *> *)requestHeaderFieldsWithCookies:(NSArray<NSHTTPCookie *> *)cookies;
    
    //从指定的响应头和URL地址中解析出Cookie数据
    + (NSArray<NSHTTPCookie *> *)cookiesWithResponseHeaderFields:(NSDictionary<NSString *, NSString *> *)headerFields forURL:(NSURL *)URL;
    
    //Cookie数据中的属性字典
    @property (nullable, readonly, copy) NSDictionary<NSString *, id> *properties;
    
    //请求响应的版本
    @property (readonly) NSUInteger version;
    
    //请求相应的名称
    @property (readonly, copy) NSString *name;
    
    //请求相应的值
    @property (readonly, copy) NSString *value;
    
    //过期时间
    @property (nullable, readonly, copy) NSDate *expiresDate;
    
    //请求的域名
    @property (readonly, copy) NSString *domain;
    
    //请求的路径
    @property (readonly, copy) NSString *path;
    
    //是否是安全传输
    @property (readonly, getter=isSecure) BOOL secure;
    
    //是否只发送HTTP的服务
    @property (readonly, getter=isHTTPOnly) BOOL HTTPOnly;
    
    //响应的文档
    @property (nullable, readonly, copy) NSString *comment;
    
    //相应的文档URL
    @property (nullable, readonly, copy) NSURL *commentURL;
    
    //服务端口列表
    @property (nullable, readonly, copy) NSArray<NSNumber *> *portList;
    

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

    //获取单例对象
    + (NSHTTPCookieStorage *)sharedHTTPCookieStorage;    
    
    //所有Cookie数据数组 其中存放NSHTTPCookie对象
    @property (nullable , readonly, copy) NSArray<NSHTTPCookie *> *cookies;   
    
    //手动设置一条Cookie数据
    - (void)setCookie:(NSHTTPCookie *)cookie;   
    
    //删除某条Cookie信息
    - (void)deleteCookie:(NSHTTPCookie *)cookie;    
    
    //获取某个特定URL的所有Cookie数据
    - (nullable NSArray<NSHTTPCookie *> *)cookiesForURL:(NSURL *)URL;    
    
    //删除某个时间后的所有Cookie信息 iOS8后可用
    - (void)removeCookiesSinceDate:(NSDate *)date NS_AVAILABLE(10_10, 8_0);    
    
    //为某个特定的URL设置Cookie
    - (void)setCookies:(NSArray<NSHTTPCookie *> *)cookies forURL:(nullable NSURL *)URL mainDocumentURL:(nullable NSURL *)mainDocumentURL
    
    /*
    枚举如下:
    typedef NS_ENUM(NSUInteger, NSHTTPCookieAcceptPolicy) {
        NSHTTPCookieAcceptPolicyAlways,//接收所有Cookie信息
        NSHTTPCookieAcceptPolicyNever,//不接收所有Cookie信息
        NSHTTPCookieAcceptPolicyOnlyFromMainDocumentDomain//只接收主文档域的Cookie信息
    };
    */
    @property NSHTTPCookieAcceptPolicy cookieAcceptPolicy;//Cookie数据的接收协议
    
    **系统下面的两个通知与Cookie管理有关**
    
    //Cookie数据的接收协议改变时发送的通知
    FOUNDATION_EXPORT NSString * const NSHTTPCookieManagerAcceptPolicyChangedNotification;
    //管理的Cookie数据发生变化时发送的通知
    FOUNDATION_EXPORT NSString * const NSHTTPCookieManagerCookiesChangedNotification;
    
    **存放和获取一个task任务所对应的cookie,iOS8.0以后支持**
    - (void)storeCookies:(NSArray<NSHTTPCookie *> *)cookies forTask:(NSURLSessionTask *)task NS_AVAILABLE(10_10, 8_0);
    - (void)getCookiesForTask:(NSURLSessionTask *)task completionHandler:(void (^) (NSArray<NSHTTPCookie *> * _Nullable cookies))completionHandler NS_AVAILABLE(10_10, 8_0);
    

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

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

    2、设置UIWebView的Cookies

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

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

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

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

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

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

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

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

    使用字典初始化新的cookie

    NSHTTPCookie *newcookie = [NSHTTPCookie cookieWithProperties:cookieProperties];
    

    最后使用cookie管理器存储cookie

    [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:newcookie];
    

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

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

    b、实现setWebViewCookiesButtonsetWebViewCookies方法

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

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

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

    创建了新`cookie`

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

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

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

    image.png

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

    image.png

    3、获取WKWebView的Cookies

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

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

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

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

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

    • session 级别的 cookie:保存在 WKProcessPool里,每个 WKWebview 都可以关联一个 WKProcessPool的实例,如果需要在整个 App 生命周期里访问 h5 保留 h5 里的登录状态的,可以使用 WKProcessPool 的单例来共享登录状态。解释下,WKProcessPool 是个没有属性和方法的对象,唯一的作用就是标识是不是需要新的 session 级别的管理对象,一个实例代表一个对象。

    • 未过期的 cookie:有效期内的 cookie 被持久化存储在 NSLibraryDirectory 目录下的 Cookies/文件夹。com.xiejiapei.NSURLProtocolDemo.binarycookiesNSHTTPCookieStorage 文件对象。cookie.binarycookies则是WKWebview的实例化对象。这也是为什么WKWebviewNSHTTPCookieStorage 没有同步的原因——因为被保存在不同的文件当中。

    未过期的cookie存储位置

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

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

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

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

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

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

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

    • 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信息。

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

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

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

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

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

    
    // 单例
    + (instancetype)shareManager
    {
        // 静态局部变量
        static WKCookieManager *_instance;
        // 通过dispatch_ once方式确保instance在多线程环境下只被创建一次
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            // 创建实例
            // super: 不能使用self,否则重写的allocWithZone第一次初始化的时候 会循环调用instance
            _instance = [[super allocWithZone:NULL] init];
        });
        return _instance;
    }
    
    // 重写方法[必不可少]
    // 规避逃脱sharedInstance再去创建其他对象,当alloc的时候只能返回单例
    + (instancetype)allocWithZone:(struct _NSZone *)zone
    {
        return [self shareManager];
    }
    

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

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

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

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

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

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

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

    跳转新页面能拿到Cookie了

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

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

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

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

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

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

    相应JS脚本如下:

    - (NSString *)cookieString
    {
        NSMutableString *script = [NSMutableString string];
        [script appendString:@"var cookieNames = document.cookie.split('; ').map(function(cookie) { return cookie.split('=')[0] } );\n"];
        for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]) {
    
            if ([cookie.value rangeOfString:@"'"].location != NSNotFound) {
                continue;
            }
            [script appendFormat:@"if (cookieNames.indexOf('%@') == -1) { document.cookie='%@'; };\n", cookie.name, cookie.xjp_formatCookieString];
        }
        return script;
    }
    

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    class_methodSwizzling

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

    custom_cookies

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

    saveCookie

    拓展:Cookie 污染问题

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

    实践过程如下

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

    if ([AppHostCookie loginCookieHasBeenSynced] == NO) {
            //
            NSURL *cookieURL = [NSURL URLWithString:kFakeCookieWebPageURLString];
            NSMutableURLRequest *mutableRequest = [NSMutableURLRequest requestWithURL:cookieURL cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:120];
            WKWebView *cookieWebview = [self getCookieWebview];
            [self.view addSubview:cookieWebview];
            [cookieWebview loadRequest:mutableRequest];
            DDLogInfo(@"[JSBridge] preload cookie for url = %@", self.loadUrl);
        } else {
            [self loadWebPage];
        }
    //  注意,CookieWebview 和 正常的 webview 是不同的
    - (WKWebView *)getCookieWebview
    {
        // 设置加载页面完毕后,里面的后续请求,如 xhr 请求使用的cookie
        WKUserContentController *userContentController = [WKUserContentController new];
    
        WKWebViewConfiguration *webViewConfig = [[WKWebViewConfiguration alloc] init];
        webViewConfig.userContentController = userContentController;
    
        webViewConfig.processPool = [AppHostCookie sharedPoolManager];
        
        NSMutableArray<NSString *> *oldCookies = [AppHostCookie cookieJavaScriptArray];
        [oldCookies enumerateObjectsUsingBlock:^(NSString *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) {
            NSString *setCookie = [NSString stringWithFormat:@"document.cookie='%@';", obj];
            WKUserScript *cookieScript = [[WKUserScript alloc] initWithSource:setCookie injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES];
            [userContentController addUserScript:cookieScript];
        }];
    
        WKWebView *webview = [[WKWebView alloc] initWithFrame:CGRectMake(0, -1, SCREEN_WIDTH,ONE_PIXEL) configuration:webViewConfig];
    
        webview.navigationDelegate = self;
        webview.UIDelegate = self;
    
        return webview;
    }
    

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

    static NSString * _Nonnull kFakeCookieWebPageURLString = @"http://ai.api.com/xhr/user/getUid.do?26u-KQa-fKQ-3BD"
    - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
    {
    
        NSURL *targetURL = webView.URL;
        if ([AppHostCookie loginCookieHasBeenSynced] == NO && targetURL.query.length > 0 && [kFakeCookieWebPageURLString containsString:targetURL.query]) {
            [AppHostCookie setLoginCookieHasBeenSynced:YES];
            // 加载真正的页面;此时已经有 App 的 cookie 存在了。
            [webView removeFromSuperview];
            [self loadWebPage];
            return;
        }
    }
    

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

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

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

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

    //
    //  UWWkWebViewCookieManager.h
    //
    //  Created by DarkAngel on 2018/4/12.
    //
    
    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    /**
     WKWebView的Cookie管理,只用于iOS 11以上
     */
    @interface UWWkWebViewCookieManager : NSObject
    /**
     从NSHTTPCookieStorage同步cookie
     */
    + (void)synchronizeCookiesFromNSHTTPCookieStorage NS_AVAILABLE_IOS(11_0);
    
    @end
    
    NS_ASSUME_NONNULL_END
    

    .m文件:

    //
    //  UWWkWebViewCookieManager.m
    //
    //  Created by DarkAngel on 2018/4/12.
    //
    
    #import "UWWkWebViewCookieManager.h"
    #import <WebKit/WebKit.h>
    #import "GCDMethods.h"
    
    @interface UWWkWebViewCookieManager () <WKHTTPCookieStoreObserver>
    
    @end
    
    @implementation UWWkWebViewCookieManager
    
    + (void)load
    {
        if (@available(iOS 11.0, *)) {
            [[[WKWebsiteDataStore defaultDataStore] httpCookieStore] addObserver:(id<WKHTTPCookieStoreObserver>)self];
            [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(cookiesDidChangeInHTTPCookieStorage:) name:NSHTTPCookieManagerCookiesChangedNotification object:nil];
        }
    }
    
    /**
     从[NSHTTPCookieStorage sharedHTTPCookieStorage]同步Cookie到WKHTTPCookieStore
     */
    + (void)synchronizeCookiesFromNSHTTPCookieStorage NS_AVAILABLE_IOS(11_0)
    {
        if (@available(iOS 11.0, *)) {
            GCD_MAIN_SYNC(^{
                [[[WKWebsiteDataStore defaultDataStore] httpCookieStore] getAllCookies:^(NSArray<NSHTTPCookie *> * _Nonnull wkCookies) {
                    NSMutableSet *before = [NSMutableSet setWithArray:wkCookies];
                    NSSet *after = [NSSet setWithArray:[NSHTTPCookieStorage sharedHTTPCookieStorage].cookies];
                    //需要保留的
                    NSMutableSet *toKeep = [NSMutableSet setWithSet:before];
                    [toKeep intersectSet:after];
                    //需要添加的
                    NSMutableSet *toAdd = [NSMutableSet setWithSet:after];
                    [toAdd minusSet:toKeep];
                    //需要删除的
                    NSMutableSet *toRemove = [NSMutableSet setWithSet:before];
                    [toRemove minusSet:after];
                    for (NSHTTPCookie *cookie in toRemove.allObjects) {
                        [[[WKWebsiteDataStore defaultDataStore] httpCookieStore] deleteCookie:cookie completionHandler:nil];
                    }
                    for (NSHTTPCookie *cookie in toAdd.allObjects) {
                        [[[WKWebsiteDataStore defaultDataStore] httpCookieStore] setCookie:cookie completionHandler:nil];
                    }
                }];
            });
        } else {
            
        }
    }
    
    /**
     从WKHTTPCookieStore同步Cookie到[NSHTTPCookieStorage sharedHTTPCookieStorage]
     */
    + (void)cookiesDidChangeInCookieStore:(WKHTTPCookieStore *)cookieStore NS_AVAILABLE_IOS(11_0)
    {
        GCD_MAIN(^{
            [cookieStore getAllCookies:^(NSArray<NSHTTPCookie *> * _Nonnull cookies) {
                NSSet *before = [NSSet setWithArray:[NSHTTPCookieStorage sharedHTTPCookieStorage].cookies];
                NSMutableSet *after = [NSMutableSet setWithArray:cookies];
                //需要保留的
                NSMutableSet *toKeep = [NSMutableSet setWithSet:before];
                [toKeep intersectSet:after];
                //需要添加的
                NSMutableSet *toAdd = [NSMutableSet setWithSet:after];
                [toAdd minusSet:toKeep];
                for (NSHTTPCookie *cookie in toAdd.allObjects) {
                    [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
                }
            }];
        });
    }
    /**
     从[NSHTTPCookieStorage sharedHTTPCookieStorage]同步Cookie到WKHTTPCookieStore
     */
    + (void)cookiesDidChangeInHTTPCookieStorage:(NSNotification *)notification
    {
        if (@available(iOS 11.0, *)) {
            [self synchronizeCookiesFromNSHTTPCookieStorage];
        }
    }
    
    @end
    

    Demo

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

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

    参考文献

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

    相关文章

      网友评论

          本文标题:IOS进阶:(网络篇)HTTPCookie

          本文链接:https://www.haomeiwen.com/subject/pylqvktx.html