最近在学习WKWebView中的cookie方案,本来以为只是简单的设置一下就好了,参考了很多资料,发现里面的坑越来越大,为了弄清楚这些坑,我做了一系列实验对比分析,加上了一些推测,也算是基本解决了心中的很多谜团。虽然整个过程比较折腾,但是学习重在解决问题的过程,麻烦一点也无所谓了。本文总结了大概的探索过程,如果感兴趣,可以跟着我的方式一起学习,讨论出更加合理的结论。如果不感兴趣,可以直接看文末的推测的结论。
关于Cookie
首先需要理解cookie的概念,cookie是在http协议中非常重要的角色。http是无状态协议,也就是说http不会根据之前的访问情况来处理下次请求,在很多涉及账号的网页中,页面需要根据是否登录的状态来显示内容。为了避免每次访问都要登录,可以在第一次登录完成后讲登录信息写入cookie,添加到之后的请求中,这样就解决了http不能记录状态的问题。从开发者层面来说,cookie本质是包含了一系列key-value的数组。
如图所示,客户端第一次向服务器发送请求的时候,没有cookie,服务器收到后,会生成可以表示客户端身份的cookie,服务器将cookie封装到响应包的头部的set-cookie字段,返回给客户端,客户端根据set-cookie的内容设置cookie,并在之后的请求中带上cookie内容,这样服务器就能识别到这个客户端了。
关于WKWebView
本文重点在于对WKWebView中的cookie问题做出一些总结,不会详细说明WKWebView的API的协议的使用。WKWebView是在iOS8后UIWebView的替代品,具有低内存(实际是开启另外的进程),与Safari具有相同的JavaScript引擎,高效的app与web的通信(注入JavaScript脚本,messageHandler回调JavaScript方法)等。
Starting in iOS 8.0 and OS X 10.10, use WKWebView to add web content to your app. Do not use UIWebView or WebView.
因此,在iOS8以后,建议都使用WKWebView,但是使用过程中有很多坑,具体请看WKWebView 那些坑。这里主要讨论一下其中的cookie问题。
关于WKWebView 那些坑
在UIWebView中,在每次请求之前,会讲NSHTTPCookieStorage里面的cookie自动添加到请求中,因此UIWebView上的cookie问题并不突出。然而经过测试发现,WKWebView中并不会自动向请求中添加cookie,这也是很多文章提到的问题,总结了《WKWebView 那些坑》中的相关内容:
- WKWebView Cookie 问题在于 WKWebView 发起的请求不会自动带上存储于 NSHTTPCookieStorage 容器中的 Cookie。
- 实践发现 WKWebView 实例其实也会将 Cookie 存储于 NSHTTPCookieStorage 中,但存储时机有延迟,在iOS 8上,当页面跳转的时候,当前页面的 Cookie 会写入 NSHTTPCookieStorage 中,而在 iOS 10 上,JS 执行 document.cookie 或服务器 set-cookie 注入的 Cookie 会很快同步到 NSHTTPCookieStorage 中
WKWebView中cookie的解决方案有:
方案1:WKWebView loadRequest 前,在 request header 中设置 Cookie, 解决首个请求 Cookie 带不上的问题;
缺陷:只能解决指定的一个URL的请求,涉及到同域的Ajax跳转还是会缺失cookie
方案2: 通过 document.cookie 设置 Cookie 解决后续页面(同域)Ajax、iframe 请求的 Cookie 问题;
缺陷:不能解决跨域cookie的问题
方案3:每次页面跳转的时候回调用decidePolicyForNavigationAction回调,那么就可以在回调里拦截请求,加入cookie后重新发送。
缺陷:loadRequest是加载mainFrame请求,所以依然解决不了页面 iframe 跨域请求的 Cookie 问题
本文主要是对这些结论中的一些疑惑点进行了探索和测试,确实有了更多的了解。
关于NSHTTPCookieStorage
前面说过,其实是NSHTTPCookieStorage不再自动管理cookie,导致了这么多问题。下面简单介绍一下NSHTTPCookieStorage的概念和常用的方法,具体使用还是参考官方文档。
简单介绍
NSHTTPCookieStorage提供了一个管理cookie的单例对象,这个对象里的cookie是进程同步共享的,常用方法有:
//只读数组,包含所有的cookie的一个数组
@property (nullable , readonly, copy) NSArray<NSHTTPCookie *> *cookies;
//设置cookie
- (void)setCookie:(NSHTTPCookie *)cookie;
//删除cookie
- (void)deleteCookie:(NSHTTPCookie *)cookie;
注意:NSHTTPCookie的创建一般是用存储了描述cookie的字典初始化,
NSMutableDictionary *properties = [NSMutableDictionary dictionary];
[properties setObject:@"cookie_test" forKey:NSHTTPCookieName];
[properties setObject:@"value" forKey:NSHTTPCookieValue];
[properties setObject:@".xxx.xxx.com" forKey:NSHTTPCookieDomain];
[properties setObject:@"/" forKey:NSHTTPCookiePath];
NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:properties];
这些描述字段有NSHTTPCookieName,NSHTTPCookieValue,NSHTTPCookieDomain等,这里的NSHTTPCookieDomain和NSHTTPCookieName共同组成了主键,也就是说NSHTTPCookieStorage里面可以存在多个NSHTTPCookieName相同的cookie对象。
准备工作
工欲善其事必先利其器,研究cookie少不了客户端和服务器的交互,所以接下来需要:用mac搭建本地服务器,iOS设备和mac设备处于同一个局域网即可。和Windows系统上繁琐的配置不同,mac系统上自带Apache服务器和php环境,我们只需要几步就可以设置成功。
1.开启Apache
打开terminal,输入sudo apachectl start,打开浏览器,输入localhost或127.0.0.1,显示it works!说明服务器开启成功
这个页面是位于服务器根目录下/Library/WebServer/Documents下的index.html.en.
用记事本打开这个文件,可以看到html的代码:
<html><body><h1>It works!</h1></body></html>
我们把文件名修改为index.php,浏览器访问localhost/index.php,发现浏览器不能解析其内容,因为还没配置php环境。
2.配置php
找到php的配置文件的路径:/etc/apache2/httpd.conf
用记事本打开,搜索定位到php7_module这句话,然后将前面的#去掉,保存退出。
3.重启Apache
在terminal中输入:sudo apachectl restart
再次用浏览器访问localhost/index.php,当你看到it works!显示说明配置成功了。
4.客户端设置
这里的客户端指的是iOS设备,只要客户端与服务器处在同一个局域网下,就可以直接通过ip地址访问。ip地址通过打开网络偏好设置查看。
然后在手机浏览器中输入ip/index.php,看到it works!表示访问成功。
客户端和服务器coding
服务器
服务器设置cookie很容易,在页面开始的时候设置即可:
?php
setcookie("name_test", "value_test", time()+3600);
?>
这句代码设置了名为name_test, 值为value_test,过期时间为1小时的cookie。
读取cookie有两种方式:
1.$_COOKIE[属性名],php的方式
2.document.cookie,js的方式
我们也需要看看这两种方式在处理cookie上有什么区别,页面很简单(懒得做布局了,能看就行😂):
这里做了一个跳转页面,只在页面1上设置cookie,页面2只读取cookie,这样可以测试页面跳转后读取cookie的情况。
客户端
为了测试出cookie的情况,我设置了五个按钮:
清理磁盘:清理了library目录下的cookie文件
清理内存:清理NSHTTPCookieStorage的cookie数组,是模拟进程退出的情况
新建(普通):新建一个WKWebView,并发送请求
新建(带Script):区别于普通的新建,新建WKWebView的时候,会在configuration中通过userScript的方式设置cookie,cookie的name为name_test,value为Config setting
刷新:页面刷新,不涉及新建,重新发送请求
测试与分析
申明:测试环境是xcode9.4.1, 机型是iPhone 7P, 系统11.4,模拟器和真机差别很大,请使用真机。
测试中发现以下现象:
1.整个实验中,php有时读取不到cookie,而js读取一直成功,即使是第一次访问页面。
分析:其实不太明白这两种方式的应用场景,从现象来看,js的逻辑似乎是,优先读取客户端请求里的cookie,读不到时会读取服务器设置的cookie。后续有了新的了解再完善。
注:由于js读取一直成功,后面现象的描述都省略js的情况
2.对于两种新建方式(带script和不带的)来说,首次新建页面,PHP读取为空,连续多次点击刷新按钮或者是跳转页面,PHP读取到cookie。
分析:符合正常的cookie特点,首次请求没有cookie,响应回来后设置,后续的请求就会带上cookie,可以说明在这种刷新的情况下,WKWebView请求是会自动带上cookie的。
3.在cookie已经成功读取的情况下,连续多次点击新建(普通)按钮,cookie一直读取成功。点击清理磁盘,然后点击新建(普通)按钮,cookie读取不到。点击清理内存,然后新建,cookie有时读不到。
分析:这里主要是想分析出WKWebView请求的时候自动带上的cookie来自于哪里。现象来看,当cookie已经生成时,新建WKWebView不影响cookie的读取,而一旦清理了磁盘时,cookie就没有了。而清理内存,内存里的内容来自于读取磁盘里的内容,有时可以读取的到。
4.新建(带script)的方式,只有点击刷新才会读取到cookie,新建则读取不到。
分析:userScript是设置在configuration里,而configuration作为参数初始化WKWebView,每次新建就相当于一个新的设置,这种方式的cookie存储和WKWebView的生命周期绑定了,同生共灭。
5.读取不到cookie的情况下(可以通过清理磁盘+新建的方式实现),快速连续点击新建(普通)按钮,会发现一直读不到cookie,而慢速点击的时候,就可以读到cookie了
分析:测试中,慢速和快速的大约范围是3到5秒,也就是说,当客户端没有cookie的时候,第一次访问会设置cookie,但是这个设置是需要时间的。如果在规定的时间内没有设置成功,而下一次请求已经到来,就会cancel当前的设置,导致一直没有cookie
翻阅一些相关资料,并没有找到完整而合理的解释,在此基于测试现象,我做出了可能的解释:
1.WKWebView对象内部有一套自己的cookie管理配置,有相应的存储和策略,是和对象绑定的,当WKWebView对象存在时,内部管理会在第一次请求返回的时候设置cookie到。当WKWebView对象销毁的时候,管理配置也就没有了,存储的cookie也就没了。
2.NSHTTPCookieStorage的单例对象,在每次响应返回的时候,会自动把响应包里的set-cookie字段里的内容写到磁盘中并同步在内存,每一次请求来了,NSHTTPCookieStorage对象会在自身cookie中查看是否有值并且和磁盘cookie一致(可能是校验码之类的),如果有,就直接将NSHTTPCookieStorage中的cookie自动添加到请求头中,如果没有,就会从磁盘里读取。如果过于迅速的请求页面,可能导致还没有读取完成就发送了请求,这时就会读取失败了。
3.WKUserScript本质是在页面插入一段js代码,和configuration绑定,因此也是和WKWebView对象的生命周期一致。
4.WKWebView发送请求前会查找cookie,首先查找对象内部的存储,如果没有,就去NSHTTPCookieStorage对象中查找,如果NSHTTPCookieStorage对象里没有或者个和磁盘cookie不一致,就会异步地去磁盘里找。
ps: 在进程开启和退出的时候,都会有NSHTTPCookieStorage和磁盘内容同步。因此在应用中,需要注意第一次请求没有cookie,然后又连续多次发送同一请求的情况,这种情况可能导致磁盘和内存设置失败,从而读取不到cookie。
如何设置cookie
通过测试,我们发现WKWebView里cookie的坑确实不少,在实际使用中,可以考虑与后台一起商量方案:
1.交给NSHTTPCookieStorage处理。这个处理方式是自动的,不用做额外操作。
使用场景:无需第一次发送cookie给服务器,无需更改cookie内容,不会连续多次请求第一个页面(后续的ajax跳转可以连续多次)
2.请求头里添加Cookie的方式 + userScript的方式。请求头的方式解决了第一次设置,userScript解决了后续页面ajax跳转的问题。
[request addValue:@"cookieName = value" forHTTPHeaderField:@"Cookie"];
使用场景:客户端给服务器设置cookie,但是cookie一旦设置,就不能改变了。
3.拦截请求 + 请求头里添加Cookie的方式。在回调里拦截请求,同域的ajax跳转都可以在回调里收到消息,然后在请求头里添加cookie后重新设置即可。
使用场景:处理变化的cookie,即在交互中,cookie信息可能会一直变化(增加字段等)
代码下载
https://github.com/wszxy/WKWebViewCookie
参考文章: