深入WKWebView的Cookie管理

4,586 阅读9分钟

什么是WebKit?

Apple在2003年的时候发布了第一版Safari,然后再2005年的时候开源了Safari游览器的内核WebKit, 一直到现在Safari游览器的内核都是WebKit,而Google使用的游览器内核是Blink,它是从WebKit拉出的分支,加以优化改进的游览器内核。

而WebKit的核心组件就是:WebCore以及JavaScriptCore。 WebKit框架提供了OC的API接口来访问基于C++的WebCore渲染引擎以及JavaScriptCore脚本引擎,这样应用程序可以通过Cocoa API方便的访问。

而在使用中 ,Apple要求该平台上的Web游览器都使用WebKit,所以iOS版本的Chrome游览器使用的内核也是WebKit, 而桌面端的Chrome则使用的Blink引擎(仅使用了WebCore,以及自己命名为V8的JavaScript引擎和一个多进程系统)众多基于开源的webkit开发的知名游览器,都使用了WebKit的WebCore,但是JavaScript引擎都是自己重写的。

WebCore

WebCore是一个渲染引擎(the rendering engine),主要是将HTML解析为DOM,进行布局以及渲染等等工作。

JavaScriptCore

JavaScriptCoer是WebKit的JavaScript引擎,它是解释执行JavaScript代码的。但是它有好几个不同的名字,在Safari的上下文中,一般称之为Nitro。

WebKit的多线程

WebKit是传统的架构,所有的都在一个进程中,这是一个非常简单的模型,应用程序复用WebKit的API也是非常容易的。

而WebKit2则使用了多进程模型,部分WebKit的操作在UI进程中,应用的逻辑处理也在这个进程。而剩下WebKit,包括WebCore以及JS引擎,都在web进程中工作。这个web process和UI process是隔离的。所以更加安全,更能充分利用多核CPU。

WKWebView的多线程

WKWebView是多进程的机制,查看编译好的WebKit源码可看到一个文件夹目录:

这里面有三个主要的进程:

  • UIProcess:也就是App所在的进程,WKWebView在该进程中提供了大量API供开发者与内核交互,也是开发者最熟悉的进程。
  • NetworkProcess:无论是多个WKWebView实例还是单个,都只有一个唯一的NetWorkingProcess,主要便于网络请求管理,网络缓存管理,Cookie管理的一致性。
  • WebProcess:也就是WebContent进程,主要负责WebCore,JSCore相关模块的运行,是WKWebView的核心进程。每个WKWebView都会有独立的Web进程,不过该进程会根据内存进行复用。某一个webContent进程的异常并不会影响到主进程,常见的异常现象是白屏现象。

\

同时,各个进程之间是通过CorelPC进程来通信的。

总结:在一个客户端中,多个WKWebView实例会共享一个UI进程(和App进程共享),共享一个Networking进程,每个WKWebView实例独享一个Web进程。

WKWebView的Cookies 管理

Cookie的存储

从文档可知,App中的Cookie管理使用的HTTPCookieStrorage,WKWebView的Cookie管理使用的是WKHttpCookieStore。这两个Cookie是可以同步的,但是同步会有明显的延迟,所以为了代码的稳定性,需要我们自己去处理cookie。

在使用UIWebView的时候,它是由HTTPCookieStorage类来管理Cookie,和App进程其他组件读取Cookie时读取的是同一块区域,然而WKWebView的Cookie设计不太一样,因为它和APP不在一个进程,所以它们的Session是不一致的,它们的Cookie也存储在不同的内存区域

但是这里要注意一点,就是在iOS上App进程持久化的Cookie和WKWebView进程持久化的Cookie确实不在一个文件夹,然而Mac上App进程和WKWebView进程持久化的Cookie都在同一个文件夹!

对于iOS上,Cookie文件有两种:

  • bundleID.binarycookies 这是HttpCookieStorage持久化的Cookie
  • Cookies.binarycookies 这是WKWebView持久化的Cookie

至于Cache文件,这个确实是存储在不同的文件夹(iOS,iPad,Mac都一样),这里我们实际看看内存文件即可:

关于这一点在Apple的文档中也有描述(共享Cookie内存):

Cookie的类型

实际上我们使用的时候会遇到两种类型的Cookie:

1、Session Cookie

这种类型的Cookie是只对当前进程可知的,并不会引起Cookie共享。也就是说App进程使用的SessionCookie,和WKWebView使用的SessionCookie是不一样的,这就是会导致Cookie不一致的原因。

2、Persistent Cookie

那么当进程杀死之后,如果没有持久化的话(就是保存在Cookie文件夹中),那么下一次进入App的时候,很明显,我的网络请求的Cookie是空的!

App进程持久化Cookie

App进程应该如何修改Session Cookie呢?我想这个问题是很简单的:

1、通过HttpCookieStorage来写入的

2、通过网络请求Response Header中通过"Set-Cookie"写入的

那么将App进程的Cookie持久化的方案呢?一共有如下几种:

1、服务器设置Set Cookie

主要是需要设置expires过期时间,确保时间超过当前时间,同时需要设置sessionOnly属性为FALSE

设置的格式如下:

Set-cookie:name=name;expires=date;path=path;domain=domain;secure

实例如下:

Set-Cookie: age=11; Expires=Wed, 19 Oct 2022 07:28:00 GMT

Mac客户端收到该请求的response之后,就会将这个Cookie持久化为文件:Cookies.binarycookies,保存在本地,下次请求的时候,HttpCookieStorage.shared.cookies就是保存在本地的Cookie。

要是iOS客户端或者iPad客户端,这个文件是bundleID.binarycookies,这一点上iOS和OS X上略有不同。

该文件可以通过BinaryCookieReader.py来读取该cookie文件。

  • cd在BinaryCookieReader.py的文件目录下
  • 执行python BinaryCookieReader.py /Users/ryan/Library/Containers/com.ryan.macWebviewDemo.MacWebView/Data/Library/Cookies/Cookies.binarycookies
  • 解析结果就会出现在命令行中

2、App端手动存储Cookie

直接通过归档的方式来保存Cookie到本地,然后在启动的时候加载持久化后的Cookie。

func saveCookie() {
    let data = try? NSKeyedArchiver.archivedData(withRootObject: HTTPCookieStorage.shared.cookies!, requiringSecureCoding: true)
    UserDefaults.standard.setValue(data, forKey: "come.test.cookie")
    UserDefaults.standard.synchronize()
}

func loadCookie() -> [HTTPCookie]? {
    guard let data = UserDefaults.standard.object(forKey: "come.test.cookie") as? Data else {
        return nil
    }

    guard let cookies = try? NSKeyedUnarchiver.unarchivedObject(ofClasses: [HTTPCookie.self], from: data) as? [HTTPCookie] else {
        return nil
    }

    return cookies
}

3、直接修改HTTPCookieStorage

网上的很多文章都说直接修改HTTPCooieStorage存储是不生效的,但是那些文章都有问题,经过我的测试,直接修改HTTPCooieStorage持久化Cookie是生效的。

这是设置Cookie的时候,一定要注意:

  • expires 一定要设置一个超出当前时间的过期时间
  • discard 这个字段千万不要设置,不知道为什么,不管设置成什么值,都会导致这个cookie变成session only
let property: [HTTPCookiePropertyKey: Any] = [.name: "age",
                                              .value: "hui",
                                              .domain: "mock.uutool.cn",
                                              .path: "/",
                                              .expires: Date.init(timeIntervalSinceNow: 3600)]
let httpCookie = HTTPCookie.init(properties: property)!
HTTPCookieStorage.shared.setCookie(httpCookie)

通过这种设置,无论是Mac端,iPhone端,还是iOS端都是可以持久化Cookie的!即以上设置完之后,cookie就会持久化为文件保存在沙盒里。

WKWebView持久化Cookie

对于iOS 11之后的系统WKWebView使用的是WKHTTPCookieStore来管理Cookie。根据以上的总结,我们可以得知在iOS上,WKWebView的cookie持久化之后是会保存在Cookies.binarycookies 文件中的,可是WKWebView的Cookie什么是时候持久化呢?

1、直接手动设置WKWebsiteDataStore

let property: [HTTPCookiePropertyKey: Any] = [.name: "age",
                                              .value: "hui",
                                              .domain: "mock.uutool.cn",
                                              .path: "/",
                                              .expires: Date.init(timeIntervalSinceNow: 3600)]
let httpCookie = HTTPCookie.init(properties: property)

let dataStorage = WKWebsiteDataStore.default()
dataStorage.httpCookieStore.setCookie(httpCookie!, completionHandler: nil)

let config = WKWebViewConfiguration.init()
config.websiteDataStore = dataStorage

// 开始请求
let url = URL.init(string: "https://mock.uutool.cn/hui")!
webView = WKWebView.init(frame: CGRect.init(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height), configuration: config)
webView.navigationDelegate = self
webView.load(URLRequest.init(url: url))

它会在请求发出的时候就将Cookie持久化存储在本地文件中,经过我的测试,断点在decidePolicyFor代理方法,本地的Cookies.binarycookies文件就已经生成了。

当然啦,我也可以设置cookie不持久化到本地:

let dataStorage = WKWebsiteDataStore.nonPersistent()

2、服务器设置Set-Cookie

这一点也是WKWebView也上述一致,如果收到请求之后,服务器也设置了Set-Cookie的话,那么返回的cookie就会直接持久化到本地,那么,如果我不想持久化返回的这个Cookie呢?

和上述是一致的,使用nonPersistent方法提前设置即可。

let dataStorage = WKWebsiteDataStore.nonPersistent()
let config = WKWebViewConfiguration.init()
config.websiteDataStore = dataStorage

3、JS通过document.cookie来写入

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
    NSLog("didFinish")
    webView.evaluateJavaScript("document.cookie='age=18;expires=Wed, 19 Oct 2022 07:28:00 GMT'", completionHandler: nil)
}

直接通过webView来执行JS代码也可以使得cookie持久化。

App进程的Cookie和WebProcess进程的Cookie如何同步?

其实这个问题可以抽象为两个问题

问题1:WebProcess的Cookie如何同步到App进程呢?

因为在iOS 11之后,WKWebView统一使用WKHTTPCookieStore来管理Cookie, 而WKHTTPCookieStore提供了WKHTTPCookieStoreObserver来监听Cookie的变化。

这样的话,我们就可以在监听到WKWebViewCookie发生变化的时候,将Cookie同步到App进程。

func setupWebView() {
    ...
    webView.configuration.websiteDataStore.httpCookieStore.add(self)
}

// WKHTTPCookieStoreObserver协议的方法
func cookiesDidChange(in cookieStore: WKHTTPCookieStore) {
    cookieStore.getAllCookies { array in
        array.forEach {
            HTTPCookieStorage.shared.setCookie($0)
        }
    }
}

值得注意的是,这样的话,两种不同进程的Cookie都会持久化到本地。所以如果要HTTPCookieStorage的Cookie不持久化到本地的话,可以添加“discard”字段在HTTPCookie中,这也就是相当于设置“sessionOnly”属性了。

问题2:App进程的Cookie如何同步到WebProcess呢?

这个时候假设一个场景:App客户端请求了一个URL,服务器收到之后使用了Set-Cookies,当App客户端收到服务器的Response之后,需要把Cookies同步到WKWebView,那么如何实现呢?

可以采用淘宝技术分享中提到的解决方案:

我们可以通过 HOOK NSHTTPCookieStorage 中读写 Cookie 的接口,并且监听网络请求中 "Set-Cookie" 关键字,在 App 进程 Cookie 发生变化时同步到 WKWebView。

WKWebView可能会遇到的问题

坑点一:无法在WKWebView加载的Request里添加HTTP Body

这个已知的Bug是WebKit的已经存在的,现在已经修复了,所以现在是可以再Request里添加HttpBody的,但是添加HttpBody的时候,一定要注意指定设置HTTPHeaderField中的“Content-Type”:

WebKit Bug Report

var request = URLRequest.init(url: url)
request.httpMethod = "POST"
request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.httpBody = "do some thing".data(using: .utf8)

webView.load(request)

但是经过我的测试,在request设置为post请求的时候,使用request.addValue方法来添加Http头的时候,只有设置Content-Type是管用的,设置Accept,User-Agent等等都是不生效的, 不过有可能会有设置User-Agent的需求,这个可以通过WebView来直接设置:

webView.customUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Safari/537.36"

坑点二:无法在WKWebView加载的Request里设置HTTP Header

这个和上述问题一样,WKWebView的部分请求头是无法修改的

具体怎么改,我也不知道~

参考资料

[1] 官方文档 trac.webkit.org/wiki/WebKit…

[2] 维基百科 en.wikipedia.org/wiki/WebKit

[3] 淘宝技术团队 zhuanlan.zhihu.com/p/393287432

[4] WKWebView那些坑 juejin.cn/post/684490…

[5] 干货:探秘WKWebView mp.weixin.qq.com/s/l9D4V0ON3…