WebKit 网络拦截 Cookie 同步方案

623 阅读4分钟

WebKit Cookie 存储策略

由于 WebKit 多进程协作关系,Cookie 需要在 WebContent、Networking 两个进程间调度与同步。

比如 JS 调用 document.cookie 设置的数据要在网络请求时带上,就需要从 WebContent 进程同步到 Networking 进程;网络请求响应头的Set-Cookie数据要让 JS 调用 document.cookie 能获取到,就需要从 Networking 进程同步到 WebContent 进程。

双向同步机制

简单看一下源码中如何实现的。

存储 Cookie 的核心类是NetworkStorageSession(封装了NSHTTPCookieStorage),在 WebContent 和 Networking 进程都使用到了它。

在 Networking 进程 Cookie 发生变化时,会 IPC 告知到 WebContent 进程的NetworkProcessConnection;在 WebContent 进程 Cookie 发生变化时,会 IPC 告知到 Networking 进程的 NetworkConnectionToWebProcess

为了提升 WebContent 进程对 Cookie 的读写效率,设计了一个WebCookieCache,减少 IPC 通信次数。

它们大致形成了这样一个双向同步机制:

image.png

网络拦截带来的 Cookie 同步问题

要实现全网过代理以及深度的 Web 网络优化,WebKit 网络拦截是一个必要的基础技术,由 APP 进程承接 WebView 的网络请求,对 Cookie 方面主要影响有几点:

  1. WebContent 进程产生的 Cookie 不能被 APP 进程感知到,APP 进程发起网络请求时带不上;
  2. APP 进程网络请求响应包的 Cookie 不能被 WebContent / Networking 进程感知到,导致 JS 上下文获取不到这些 Cookie;

本质来看我们需要做到两点即可:

  1. WebContent 进程产生的 Cookie 同步到 APP 进程;
  2. APP 进程网络请求产生的 Cookie 同步到 WebContent 或 Networking 进程;

image.png

可能想到是做法是基于 Hook JS 代码插入 IPC 通信逻辑,强行对齐 WebContent 和 APP 进程的 Cookie 数据,从细节去分析后发现可能无法完美处理。我们更希望的方案是三个进程的 CookieStorage 能从更底层去同步,而不是在上层做一些不稳定且不安全的 Hack 逻辑。

解决方案

既然我们是在 APP 进程承接 Networking 进程的工作,那可以稍微探索一下 Networking 进程是如何存储 Cookie 的,看是否有所启发。

Networking 进程 Cookie 如何存储

调试一下就发现Set-Cookie响应头其实在公开代理无法获取,追溯一下调用链路,在 Networking 进程的 NSURLSession 请求完成时,会调将Set-Cookie响应头清理掉:

void ResourceResponseBase::sanitizeHTTPHeaderFields(SanitizationType type) {
…
 m_httpHeaderFields.remove(HTTPHeaderName::SetCookie);
 m_httpHeaderFields.remove(HTTPHeaderName::SetCookie2);
…
}

然后将清理后的响应头 IPC 到 WebContent 进程,WebContent 进程再 IPC 到 APP 进程。

也就是说 Networking 进程没有对响应头的 Cookie 做存储处理,那就依赖 NSURLSession 内部逻辑了,因为它们都操作的同一进程的[NSHTTPCookieStorage sharedHTTPCookieStorage],NSURLSession 处理闭源,但可以在 Swift 源码中找到痕迹:

image.png

既然如此,我们需要自己想办法处理了,让逻辑下沉并且避免 Hook。

核心处理

对于 WebContent 进程产生的 Cookie 同步到 APP 进程,可以不用 Hook document.cookie setter,用 iOS 11 及以上的一个公开监听接口(还需验证):

-[WKHTTPCookieStore addObserver:]

对于 APP 进程网络请求产生的 Cookie 同步到 WebContent 或 Networking 进程,可以直接在网络请求响应时,通过以下接口同步:

[WKWebsiteDataStore defaultDataStore].httpCookieStore

WKHTTPCookieStore 封装了一系列 IPC 接口,通过 Networking 进程的 WebCookieManager 同步 Cookie,由于 WebKit 本身的 Cookie 双向同步机制,也会同步到 WebContent 进程。

IPC 时序问题

试想这样的场景:

  1. 网络请求响应头返回Set-Cookie
  2. Cookie 通过 IPC 链路:APP -> Networking -> WebContent;
  3. 回包数据 IPC 链路:APP -> WebContent;
  4. 网络请求回包后 JS 代码同步读取 Cookie;

第 2 步其实不一定是两次 IPC,由于 WKHTTPCookieStore 的接口一次只能同步一个 Cookie(其实内部有批量同步能力只是未开放),所以随着 Cookie 量级增加耗时也会增加,而这个操作不会阻塞当前线程。

那么就有概率第 3 步先于第 2 步完成,导致 JS 代码无法实时读取到 Cookie,引发业务异常。

要解决这个问题方案其实挺多:

  1. 找到 APP 进程网络回包代理更早的时机同步,比如 Hook NSHTTPCookieStorage 插入逻辑;
  2. 注入 JS 脚本,主动去同步读取 APP 进程的 Cookie;
  3. 阻塞 APP 进程网络回包线程,根据公开接口,在 Cookie IPC 完成后返回;

考虑之下,还是方案 3 更加优雅,网络回包线程理论上都是在子线程,并且这里数次 IPC 耗时也是在几毫秒级别,算是可以接受的性能劣化范围。