聊聊 WKWebView cookie 管理那些事
1. 前言
在浏览内核加载网络资源的过程中我们离不开 HTTP 协议。它是在 Web 上进行数据交换的基础,同时也是一种无状态的 client-server 协议。这种无状态的属性促使许多端存储技术产生,其中最重要的技术之一就是 cookie 存储技术,它能方便的将数据存储于客户端,且在每次请求中都会在请求头中携带 cookie 数据并发送给 server。
cookie 技术的便捷性使得它在多种场景中被广泛使用,有时候甚至存在滥用情况,对同一 cookie 实例,前端、客户端、服务端都可以轻易的进行增删改查,我们在享受其便捷性的同时,也有必要确保其被正确、可控的使用。本文将在前系列文章的基础上,继续深入 WKWebView 源码,聊聊 cookie 管理那些事,希望给大家带来一些新的视角和认知,揭开 cookie 管理的迷雾。
2. Cookie 概述
MDN官网(https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies)对cookie的介绍如下:
HTTP cookie(也叫 Web cookie 或浏览器 cookie)是保存在浏览器本地的一小块数据,它会在浏览器向服务器发起请求时被携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器,如保持用户的登录状态。cookie 使基于无状态的 HTTP 协议记录稳定的状态信息成为了可能。
cookie 主要用于以下三个方面:
会话状态管理:如用户登录状态、购物车、游戏分数或其它需要记录的信息。
个性化设置:如用户自定义设置、主题等。
浏览器行为跟踪:如跟踪分析用户行为等。
简单介绍完 cookie的概念后,接下来我们再分别从前端、后端、客户端的视角聊聊 cookie 的基本使用。
3. Cookie 基本使用
丨3.1 前端通过 js 操作 cookie
详细 cookie 格式语法参考 MDN 语法链接:https://developer.mozilla.org/zh-CN/docs/Web/API/Document/cookie
// 读取所有可从当前页面访问的 cookie
allCookies = document.cookie;
// 写一个新 cookie
document.cookie = "someCookieName=true; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/";
丨3.2 后端配置 cookie
详细 cookie 格式语法参考 MDN 语法链接:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Cookies
在 response header 中返回需要种到端上的 cookie ,我们通过 Charles 工具抓包可以看到 header 中如下信息:
丨3.3 客户端操作 cookie
iOS 系统在 WKHTTPCookieStorage 类中提供如下 API 进行 cookie 操作:
@interface WKHTTPCookieStore : NSObject
/*! @abstract 获取所有 cookie
@param completionHandler 获取所有 cookie 后回调
*/
- (void)getAllCookies:(void (^)(NSArray<NSHTTPCookie *> *))completionHandler;
/*! @abstract 设置一个 cookie
@param cookie 需要设置的 cookie
@param completionHandler cookie 设置成功的回调
*/
- (void)setCookie:(NSHTTPCookie *)cookie completionHandler:(nullable void (^)(void))completionHandler;
/*! @abstract 删除指定的 cookie
@param completionHandler cookie 成功删除的回调
*/
- (void)deleteCookie:(NSHTTPCookie *)cookie completionHandler:(nullable void (^)(void))completionHandler;
@end
可以看到,不同场景下的 cookie 操作都是极其简单的,我们似乎已经通过简单的封装接口掌握了 cookie 技术,那么问题来了:
(1)cookie 究竟是存储在哪的?内存,还是磁盘?
(2)三种不同场景的 cookie 操作是如何协同工作的?
现在,我们能回答这些问题吗?如果不能,请继续跟随我深入 WKWebView 源码,让代码告诉我们答案。
4. WebKit Cookie 技术原理
再次回到源码探索的道路,现在我们再回顾一下在《深入理解 WKWebView(入门篇)—— WebKit 源码调试与分析》提及的源码探索的核心技巧:紧紧围绕 UIProcess、WebContent、NetworkProcess 三大进程进行理解。
丨4.1 三大进程与三种场景
如上图所示,我们将 cookie 操作的三种场景与三大进程进行关联,其中,
(1)客户端操作在 UIProcess 进程(即我们的 app 进程),通过封装的 WKHTTPCookieStorage 进行操作。
(2)前端 js 函数,通过 JSCore 解析执行后最终调用了 WebContent 进程中的 C++ 函数进行操作,如下所示:
virtual String cookies(Document&, const URL&) const;
virtual void setCookies(Document&, const URL&, const String& cookieString);
(3)WKWebView 中的网络请求最终都是通过 NetworkProcess 中的 NSURLSession 管理的,服务端网络响应的 cookie 设置操作都在该进程中完成。
丨4.2 三种场景下的协同工作
cookie 管理协同图
如图所示,描述了三大场景下 cookie 的协同管理,接下来,我们将结合该图解答第二小节中提出的问题。
问题一:cookie 究竟是存储在哪的?内存,还是磁盘?
UIProcess:
UIProcess 进程为 app 进程(app 进程中其实有 NSHTTPCookieStorage 仓储进行 cookie 管理,但这不是本文的重点,因此不展开来讲),苹果系统为开发者提供了 WKHTTPCookieStorage API 进行 WebKit 内核的 cookie 管理,WKHTTPCookieStorage 其实并不提供实际的存储能力,而是封装了一系列基于进程间通信的方法,将 UIProcess 进程中发生的 cookie 操作,发送到 NetworkProcess 进程中进行处理,并将执行结果通过回调函数返回。
WebContent:
WebContent 进程是前端操作 cookie 的进程,原则上,每一个网页页面都只能操作当前页面域名下的cookie。因此基于性能考虑,每一个 WebContent 进程中会有一个 cookieCache 实例,它是 NetworkProcess 进程中存储 cookie 的子集,仅存储当前页面域名下的 cookie,因此 cookieCache 采取了内存缓存的方式,其特征是存储量小,查找速度快。
NetworkProcess:
NSHTTPCookieStorage setCookie 流程图
NetworkProcess 进程是 cookie 存储的最核心进程,它管理来自网络中服务端 response 中配置的 cookie,同时也接受来自前端和客户端的 cookie 操作,是最全的 cookie 存储中心。通过源码分析,我们发现其内部还是通过 NSHTTPCookieStorage 进行管理的, NSHTTPCookieStorage 有如下存储规则:
(1)allCookies:所有 cookie 都会存入字典 allCookies 中,方便快速查询。当我们杀死 app 后,位于内存中的 allCookies 字典也会一同清理掉。
(2)sessionOnly false cookie:对于某个 cookie,如果其属性中 sessionOnly 为 false,且设置的过期时间未到达,那我们判断该 cookie 是否具备持久性的逻辑如下:
let persistable = self.allCookies.filter { (_, value) in
value.expiresDate != nil &&
value.isSessionOnly == false &&
value.expiresDate!.timeIntervalSinceNow > 0
}
(3)持久性 cookie:具备持久性的 cookie 需要存储到磁盘文件中。存入路径规则如下:
let bundlePath = Bundle.main.bundlePath
var bundleName = bundlePath.components(separatedBy: "/").last!
if let range = bundleName.range(of: ".", options: .backwards, range: nil, locale: nil) {
bundleName = String(bundleName[..<range.lowerBound])
}
let cookieFolderPath = URL(fileURLWithPath: bundleName, relativeTo: FileManager.default.urls(for: .applicationSupportDirectory, in:.userDomainMask)[0]).path
cookieFilePath = filePath(path: cookieFolderPath, fileName: "/.cookies." + cookieStorageName, bundleName: bundleName)
问题二:三种不同场景的 cookie 操作是如何协同工作的?
如 cookie 管理协同图 所示,不同场景下的 cookie 协同操作其本质就是三大进程间的通信:
(1)UIProcess 进程并没有直接管理 cookie,而是通过进程间通信的方式,在 NetworkProcess 进程中管理 cookie。
(2)所有 WebContent 进程都会注册监听 NetWorkProcess 中的 cookie 变更,及时进行相关变更的同步。
(3)前端 setCookie 操作会将 cookie 字符串解析为 NSHTTPCookie 实例,然后将该 cookie 存入 cookieCache 中,并同步到 NetworkProcess 中进行存储。前端执行 getCookie 操作会读取当前页面域名下的所有 cookie,若判断 cookieCache 中没有当前页面域名下的 cookie,考虑到异常情况,会兜底向 NetworkProcess 发送请求进行 cookie 查找。
(4)冷启动时,NetworkProcess 会初始化 NSHTTPCookieStorage ,并会将磁盘中的 cookie 读取出来,设置到内存字典 allCookies 中,同时将 allCookies 中的 cookie 变更通过广播的方式告知 WebContent 进程,发生了 cookie 变更,需要进行 cookie 同步。
(5)来自客户端的 cookie 操作或者来自服务端的 cookie 设置,导致了 NetworkProcess 中的 cookie 变更,都会通过广播的方式告知所有 WebContent 进程同时进行变更操作。
5. 总结
总而言之,cookie 操作简单,使用方便,多端同学都经常与其打交道。理清 WebKit 内部的 cookie 管理方式让我们在理论层面更了解 cookie 的技术原理。希望阅读此文后,相关开发同学在日常工作中,如果与 cookie 打了交道,一定要考虑清楚修改带来的影响面,谨慎操作。
NSHTTPCookieStorage 实现
NSHTTPCookieStorage 对应的 swift 版本开源代码如下, 里面有许多基础类库的设计思路,个人认为非常有参考价值,有兴趣的同学可以去研究相关实现:
https://github.com/apple/swift-corelibs-foundation/blob/main/Sources/FoundationNetworking/HTTPCookieStorage.swift
补充:跨域请求携带cookie
基于安全考虑,iOS14系统禁止了跨域请求携带cookie(https://webkit.org/tracking-prevention/)。
敬请期待:
深入理解 WKWebView(基础篇)-- 探究 WebKit 网络资源缓存
参考链接:
WebKit 源码:https://github.com/WebKit/WebKit
WebKit 官网:https://webkit.org/
Apple 源码:https://github.com/apple
MDN官网:https://developer.mozilla.org/zh-CN