WKWebView Cookie 试错

Too young too naive…


让人头疼的问题

  在项目中替换 UIWebView 之前,我看了 WKWebView 的文档、也看了网上大部分的博客,算是做了一些评估,优缺点都挺明显:优在性能更好(也有网友说徒有其表)、苹果官方主推,缺在 API 还没完全成型、Cookie 同步等历史问题,网上关于 Cookie 的解决方案五花八门(其实抄来抄去的也都差不多),看完我觉得我行了,果断换™的。

  之前在WKWebView 实战篇 - 4. 一个简单的 Cookie 同步方案 提到过一个非常简陋的 Cookie 同步的方式,简陋就算了,主要是发现了非常严重的问题 🤡🤡🤡🤡🤡

————————————————>
NSHTTPCookieStorage 向 WKHTTPCookieStore 同步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (@available(iOS 11.0, *)) {
NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
WKHTTPCookieStore *cookieStroe = self.webView.configuration.websiteDataStore.httpCookieStore;
if (cookies.count == 0) {
return;
}
for (NSHTTPCookie *cookie in cookies) {
[cookieStroe setCookie:cookie completionHandler:^{
if ([[cookies lastObject] isEqual:cookie]) {
//Sync end
}
}];
}
} else {
// Fallback on earlier versions
}

……
<————————————————

  先说一下应用场景:App 基础功能是原生的页面,包括登录认证等,对于一些业务流程采用的是 H5 页面,所以才用到了 WKWebView。为了实现 SSO,在打开 H5 页面时需要把原生获取到的认证信息传递给页面…然后就有了上面 Cookie 同步的需求。对于系统型号,原本计划支持的范围是 ios12.x~ios 16.x(当年最新版本),但是貌似苹果在略低版本的实现上还是有点儿差别,又或者低版本的系统性能跟不上 API 预期的速度。

❌ 问题一:集中出现在 ios13.x、ios14.x 的系统,最初的方式是采用 defaultDataStore 来配置 WebView,配合 WKHTTPCookieStore 的 API 同步 Cookie。在 ios13.* 的机型上,登录用户 A、打开 H5 页面,认证正常;清理原生的 Cookie、切换用户 B、打开 H5 页面,认证的还是用户 A,这个时候关闭 H5 、再次打开,用户 B 认证成功。体现了两个问题:一 新的 Cookie 并没有成功同步过去、或者说有非常严重的时差;二 defaultDataStore 默认会读取存储在本地沙盒的缓存、也有一定的时差,在官方注释中也确实标明了监听 Cookie 变化可能会存在明显的延迟;这些延迟我怀疑是读写文件导致的。

  对于打开 H5 的方式,很多网友也提到了一个方案:先加载一个大小为 0 不可见的 WebView,然后再加载另一个可见的 WebView,这种方式对于 Ticket 只能用一次的认证方案貌似不太友好;另一种大厂方案是拦截 WKWebView 的所有请求,把认证信息手动添加到请求头中,这个需要修改 H5 框架来配合?。这两种方式对于我的场景可能没起作用或者不太适用,在纠结方案时留意到了一个之前漏掉的 API 👇🏻

1
2
3
4
5
6
7
8
9
10
11
12
@interface WKWebsiteDataStore : NSObject <NSSecureCoding>

/* @abstract Returns the default data store. */
+ (WKWebsiteDataStore *)defaultDataStore;

/** @abstract Returns a new non-persistent data store.
@discussion If a WKWebView is associated with a non-persistent data store, no data will
be written to the file system. This is useful for implementing "private browsing" in a web view.
*/
+ (WKWebsiteDataStore *)nonPersistentDataStore;

...

nonPersistentDataStore 返回一个非持久性数据存储。如果 WKWebView 与非持久性数据存储关联,则不会有任何数据被写入文件系统,我理解是类似”无痕浏览”。对于彼此没有关联的业务页面,使用这种方式貌似更合理,也可以避免 Cookie 互相污染。于是乎……尝试了一下,值得开心的是,在 ios13.* 上的问题没有再出现,但是出现了个问题二……🤹‍♀️🤹‍♀️🤹‍♀️🤹‍♀️🤹‍♀️

❌ 问题二:集中出现在 ios12.* 的系统。改为使用 nonPersistentDataStore 以后,使用旧 Cookie 的问题不存在了,但是在 ios12.* 的机型上,打开 H5 页面,一个 Cookie 都同步不过去,也不对,这样说不严谨,在刚打开页面的时候可以同步过去一个,其他几个没同步过去,然后,最可笑的就是然后,监听 👇🏻

1
- (void)cookiesDidChangeInCookieStore:(WKHTTPCookieStore *)cookieStore;

,我盯着控制台的输出大概等了一年半载,其他几个 Cookie 才显示同步成功。不是说 no data will be written to the file system 吗?苍了天了。

所以哪个问题更严重呢?好像是问题二…🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣

⚠️ 对于问题一,或许有另一个 API 可以参考:在退出登录(切换用户)时,不止清空原生的会话信息,把 WKWebView 存储在沙盒中的记录也删除:

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
{
WKWebsiteDataStore* dataStore = [WKWebsiteDataStore defaultDataStore];
//Delete all data store 删除所有数据存储
NSDate* dateFrom = [NSDate dateWithTimeIntervalSince1970:0];
[dataStore removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:dateFrom completionHandler:^{
LogDebug(@"Removed all WKWebView data");
self.webView.configuration.processPool = [[WKProcessPool alloc] init]; // create new process pool to flush all data
}];

...

//ios11.* or later Deletes all cookies
WKHTTPCookieStore* cookieStore = dataStore.httpCookieStore;
[cookieStore getAllCookies:^(NSArray* cookies) {
NSHTTPCookie* cookie;
for(cookie in cookies){
[cookieStore deleteCookie:cookie completionHandler:nil];
}
}];

...

// Only deletes domain Cookies (not session Cookies) 删除 file system 中的部分 cookie
[dataStore fetchDataRecordsOfTypes:[WKWebsiteDataStore allWebsiteDataTypes]
completionHandler:^(NSArray<WKWebsiteDataRecord *> * __nonnull records) {
for (WKWebsiteDataRecord *record in records){
NSSet<NSString*>* dataTypes = record.dataTypes;
if([dataTypes containsObject:WKWebsiteDataTypeCookies]){
[[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:record.dataTypes
forDataRecords:@[record]
completionHandler:^{}];
}
}
}];
...
}

粘一段 ChatGPT 回复的会话 Cookie 和 长效 Cookie 的区别:

❓ Cookie 是否分为会话 Cookie 和长效 Cookie ?

  Cookie 可以分为会话 Cookie(Session Cookie)和长效 Cookie(Persistent Cookie)两种类型。

  1. 会话 Cookie(Session Cookie):
    会话Cookie是一种临时性的Cookie,它保存在用户的浏览器中,只在用户的会话期间有效。会话期间通常是指用户在浏览器中打开网站直到关闭浏览器的这段时间。一旦用户关闭浏览器,会话 Cookie 就会被删除,不会在用户的计算机上保留任何信息。会话Cookie通常用于存储临时的、与用户会话相关的信息,比如用户登录状态、购物车内容等。
  2. 长效 Cookie(Persistent Cookie):
    长效 Cookie 是一种更持久的 Cookie,它也保存在用户的浏览器中,但具有一定的过期时间。长效 Cookie 不会随着用户会话的结束而被删除,而是在设置的过期时间到达之前一直有效。长效 Cookie 通常用于跟踪用户的偏好设置、广告定向、用户标识等。这样,当用户重新访问网站时,网站可以读取长效 Cookie 中存储的信息,并根据之前的活动提供更加个性化的体验。

总结:

  • 会话 Cookie 是临时性的,在用户会话期间有效,关闭浏览器后会被删除。
  • 长效 Cookie 是持久性的,具有过期时间,在过期之前一直有效。

❓ 服务端是如何指定不同类型的 Cookie?

  在服务端,可以通过设置 Cookie 的属性来指定它的类型是会话 Cookie 还是长效 Cookie。这可以通过在 HTTP 响应头中设置 Cookie 的 “Max-Age”(最大年龄)或 “Expires”(过期时间)属性来实现。

  1. 设置会话 Cookie:
    如果希望 Cookie 在用户会话期间有效,即在用户关闭浏览器后被删除,可以在 HTTP 响应头中设置 “Set-Cookie” 字段,并省略 “Max-Age” 和 “Expires” 属性,例如:

    1
    Set-Cookie: cookie_name=cookie_value; path=/; HttpOnly

      ”HttpOnly” 属性可以防止 JavaScript 访问 Cookie,提高安全性。

  2. 设置长效 Cookie:
    如果希望 Cookie 在一定的时间内保持有效,可以设置 “Max-Age” 属性来指定Cookie的存活时间(单位为秒),或者使用 “Expires” 属性来指定过期日期和时间。例如:

    1
    Set-Cookie: cookie_name=cookie_value; path=/; Max-Age=3600; HttpOnly

      上述例子中,”Max-Age=3600” 表示 Cookie 将在设置后的3600秒(1小时)后过期。

  或者使用 “Expires” 属性:

1
Set-Cookie: cookie_name=cookie_value; path=/; Expires=Wed, 20 Jul 2023 12:00:00 GMT; HttpOnly

  上述例子中,”Expires” 属性指定了一个具体的过期日期和时间,当到达指定时间后,Cookie 将过期失效。

  请注意,”Max-Age” 和 “Expires” 属性是互斥的,只需要设置其中一个即可。同时,如果两者都设置了,”Max-Age” 属性优先于 “Expires” 属性。

  需要注意的是,如果设置了长效 Cookie,那么这个 Cookie 将在用户的浏览器中保持有效,直到达到指定的过期时间或者用户手动删除了这个 Cookie 为止。而会话 Cookie 则会在用户关闭浏览器后自动删除,不会在用户计算机上保留任何信息。


关于 WKWebView 的几篇文章:

WKWebView 基础篇
WKWebView 协议篇
WKWebView 实战篇
WKWebView Cookie 试错
WKWebView - WKScriptMessageHandler 循环引用

Demo

WebView 的 Demo