项目中用到了WKWebView
来渲染网页,有一个需求是在不同的账号下希望实现浏览状态隔离,如何实现呢?
Sample app
下图是我们用来测试的sample app, 两个tab分别有两个不同的WKWebView
实例,每次用户切换tab的时候,对应的tab会刷新对https://login.live.com
这个地址的访问。
问题
- 默认状态下,不同的
WKWebView
实例是不会共享浏览状态的,因此我们可以在两个tab登录不同的账号,切换tab的时候对应的登录状态不会乱掉 - 但是如果我们在其中一个tab网页登录的时候选择
Keep me signed in
,奇怪的事情发生了,当切换到另一个tab的时候,发现另一个tab竟然也处于登录的状态
原因
- 勾选
Keep me signed in
的时候发生了什么?
登录状态的保持需要客户端保存cookie,而我们知道cookie是分session cookie和persistent cookie的,两者的区别在WKWebView
的体现就是,前者会在WKWebView
实例消失的时候随之消失,而后者会保存在文件系统中。当用户勾选Keep me signed in
的时候,网页会写入某些persistent cookie
,这样当app下次启动初始化WKWebView
的时候,网页就能读取到这些内容保持用户的登录状态。
下图是app的文件系统,我们发现当勾选了Keep me signed in
,但是人为删除Cookies
文件时,网页的登录状态会丢失,这样也进一步验证了我们的想法(也可以打开Cookies
文件查看内容)
为什么默认状态下,
WKWebView
不会共享浏览状态?
默认状态下,不同的WKWebView
拥有不同的processPool, 因此浏览状态相互之间是不会共享的。为什么选择
Keep me signed in
的时候浏览状态会共享?
首先要介绍WKWebsiteDataStore的概念
A WKWebsiteDataStore object represents various types of data used by a chosen website. Data types include cookies, disk and memory caches, and persistent data such as WebSQL, IndexedDB databases, and local storage.
iOS SDK提供了两种
WKWebsiteDataStore
, WKWebsiteDataStore.default()会返回默认的, persistent dataStore,而WKWebsiteDataStore.nonPersistent()会创建一个non-persistent dataStore并返回(苹果的文档也介绍了,这种dataStore经常被用来实现无痕浏览)。If a web view is associated with a nonpersistent data store, no data is written to the file system. This property implements private browsing in a web view.
默认状态下,
WKWebView
的websiteDataStore是default版本的,也就是支持persistent cookie的dataStore- 因此单纯的使用不同的
WKProcessPool
(默认行为),并不能保证浏览状态的隔离;由于默认状态下使用了相同的支持persistent cookie的WKWebsiteDataStore.default()
,网页需要persistent的cookie会共享。
解决方法
从上面的分析来看,解决方法看起来很直接,我们可以让不同的WKWebView
持有不同的WKWebsiteDataStore.nonPersistent()
实例1
2
3
4
5
6private lazy var webView: WKWebView = {
let config = WKWebViewConfiguration()
config.websiteDataStore = WKWebsiteDataStore.nonPersistent()
let view = WKWebView(frame: .zero, configuration: config)
return view
}()
我们发现无论是否勾选Keep me signed in
,登录状态都不会共享,太好了!
但是
当用户勾选了Keep me signed in
,杀掉app之后,我们发现登录状态”竟然”丢失了!这样虽然我们实现了登录状态隔离,但是Keep me signed in
这个选项变得无用,这可不是一个专业的工程师希望看到的。而我们也知道,这也是我们采用了上述方案之后expected结果,因为WKWebsiteDataStore.nonPersistent()
本身就是不支持persistent cookie的。
我们看起来进退维谷(这是一个视频链接,请不要在公众场合打开 ;))
- 要想实现登录状态隔离,必须使用不同的
WKWebsiteDataStore
- 只有
WKWebsiteDataStore.default()
才能保存persistent cookie - 但是
WKWebsiteDataStore.default()
是一个类似单例的存在,我们无法创建不同的persistentWKWebsiteDataStore
解决方法+1
于是我们开始尝试:
然后在面向StackOverflow编程无果之后,你终于意识到自己已经是有经验的工程师了,应该可以独立解决问题了,在苦思冥想之后,终于想到了下面的方法
去看Webkit的源码,想办法使用私有的api
在浏览
_WKWebsiteDataStoreConfiguration.h
这个文件时,我们发现如下的定义1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
NS_ASSUME_NONNULL_BEGIN
WK_CLASS_AVAILABLE(macosx(WK_MAC_TBA), ios(WK_IOS_TBA))
@interface _WKWebsiteDataStoreConfiguration : NSObject
@property (nonatomic, copy, setter=_setWebStorageDirectory:) NSURL *_webStorageDirectory;
@property (nonatomic, copy, setter=_setIndexedDBDatabaseDirectory:) NSURL *_indexedDBDatabaseDirectory;
@property (nonatomic, copy, setter=_setWebSQLDatabaseDirectory:) NSURL *_webSQLDatabaseDirectory;
@property (nonatomic, copy, setter=_setCookieStorageFile:) NSURL *_cookieStorageFile;
@end
NS_ASSUME_NONNULL_END于是理论上我们可以创建
WKWebsiteDataStoreConfiguration
之后,配置不同的cookieStorageFile
路径,然后利用下面的private API,来创建不同的支持的persistent cookie的dataStore1
2
3
4
5@interface WKWebsiteDataStore (WKPrivate)
+ (NSSet<NSString *> *)_allWebsiteDataTypesIncludingPrivate;
- (instancetype)_initWithConfiguration:(_WKWebsiteDataStoreConfiguration *)configuration WK_API_AVAILABLE(macosx(WK_MAC_TBA), ios(WK_IOS_TBA));但是我们担心无法通过苹果的审核,同时这样做需要花更多的时间去看源码确保是可行的,作为一名有经验的工程师,我们认为这不是一条正路
终于,我们意识到,既然支持persistent cookie的本质就是把cookie保存到文件系统里,那我们能不能自己来实现呢?
对于我们的sample app,我们的方案如下:
- 两个
WebView
实例分别使用WKWebsiteDataStore.default()
和WKWebsiteDataStore.nonPersistent()
- 对于使用
WKWebsiteDataStore.default()
的WebView
实例,我们依靠其自身的cookie persistence机制 - 对于使用
WKWebsiteDataStore.nonPersistent()
的WebView
实例- 在恰当的时机,读取
webView
的所有cookie并保存到文件里 - 在初始化该
webView
的时候,读取文件中的所有cookie并加载到webView
中
- 在恰当的时机,读取
简单明了的方案,当然我们的代码也应该体现出我们的水平。
- 这里我们extend了
WKWebsiteDataStore
,暴露了persistCookies
和restoreCookies
两个方法,暴露了cookieStorageFile
这个属性 sample app中使用了最简单的
NSKeyedArchiver
来实现数据持久化(要注意在操作大文件时archive/unarchive是耗时的操作,如果可能尽量放到其他线程去做;但同时cookie的读写又必须在主线程上进行)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46struct WKWebsiteDataStoreAssociatedKeys {
static var cookieStorageFile: UInt8 = 0
}
extension WKWebsiteDataStore {
var cookieStorageFile: URL? {
get {
return objc_getAssociatedObject(self, &WKWebsiteDataStoreAssociatedKeys.cookieStorageFile) as? URL
}
set {
objc_setAssociatedObject(self, &WKWebsiteDataStoreAssociatedKeys.cookieStorageFile, newValue, .OBJC_ASSOCIATION_RETAIN)
}
}
func persistCookies() {
guard !self.isPersistent else {
return
}
guard let cookieStorageFilePath = self.cookieStorageFile?.path else {
return
}
self.httpCookieStore.getAllCookies { cookies in
NSKeyedArchiver.archiveRootObject(cookies.filter { !$0.isSessionOnly }, toFile: cookieStorageFilePath)
}
}
func restoreCookies() {
guard !self.isPersistent else {
return
}
guard let cookieStorageFilePath = self.cookieStorageFile?.path else {
return
}
guard let cookies = NSKeyedUnarchiver.unarchiveObject(withFile: cookieStorageFilePath) as? [HTTPCookie] else {
return
}
cookies.forEach { cookie in
self.httpCookieStore.setCookie(cookie, completionHandler: nil)
}
}
}在使用的时候,只要给
webSiteDataStore
设置cookieStorageFile
并且在合适的时机调用restoreCookies
和persistCookies
即可。
- 两个
总结
于是,我们设计的这套方案,”完美”(作为一名有经验的工程师,我们知道后续还会有各种各样奇奇怪怪的问题/需求出现)的解决了
- 不同的
WebView
之间浏览状态隔离 - Persistent cookie能够正常工作
- 该方案可以简单的推广到N(N>2)个
WebView
的情况,只需设置不同的WKWebsiteDataStore.nonPersistent()
和cookieStorageFile
即可
终于,我们有时间感慨,今晚的月色真美啊!