星的天空的博客

种一颗树,最好的时间是十年前,其次是现在。

0%

WKWebView的Cookie问题

WKWebView与App不在同一个进程运行,不会从App的标准Cookie容器NSHTTPCookieStorage读取Cookie。跨进程的数据同步是一个麻烦及容易出现问题的场景,Apple在iOS11之前没有专门的API用于Cookie操作,在iOS11之后提供了WKHTTPCookieStore,但是自测发现存在一些奇怪的bug,无法使用,此部分后续会做说明。

网上关于WKWebView的Cookie同步我有查到多种方案,但是均无法解决跨域请求的Cookie问题,后来发现使用WKProcessPool可以解决跨域问题。

UIWebView为什么没有Cookie同步问题?

网络请求完成后,会返回一个Response,如果Response中带有Set-Cookie字段,如:"Set-Cookie" = "wifi_jsessionid=3aea4df28ab14b4e8714132cb911c15a; Domain=.pingan.com.cn; Path=/";,操作系统就会将此条Cookie信息写入到NSHTTPCookieStorage

当UIWebView中有任意请求时(App进程中的请求也是一样),会去NSHTTPCookieStorage查找对应的Cookie信息,如果存在符合条件的Cookie信息,就会在请求头中带上此条Cookie信息。所以UIWebView进行跨域请求是没有任何问题的,只要NSHTTPCookieStorage有符合条件的Cookie信息即可带上。

WKWebView Cookie同步一般解决方案

WKWebView上请求不会自动带上NSHTTPCookieStorage中的Cookie, 目前的主要解决方案是通过手动的方式直接在请求头上带上Cookie或者使用JS脚本进行Cookie注入:

在请求头中设置Cookie, 解决首个请求Cookie带不上的问题:

1
2
3
4
5
WKWebView * webView = [WKWebView new]; 
NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://h5.qzone.qq.com/mqzone/index"]];

[request addValue:@"skey=skeyValue" forHTTPHeaderField:@"Cookie"];
[webView loadRequest:request];

此方法只适合解决简单针对性的场景。

通过document.cookie设置Cookie解决后续页面(同域)Ajax、iframe请求的Cookie问题:

1
2
3
4
5
6
7
8
9
10
WKUserContentController* userContentController = WKUserContentController.new;
//不设定Domain,则会将domain默认设为请求的URL的Domain
WKUserScript * cookieScript = [[WKUserScript alloc]
initWithSource: @"document.cookie = 'TeskCookieKey1=TeskCookieValue1';document.cookie = 'TeskCookieKey2=TeskCookieValue2';"
injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
// again, use stringWithFormat: in the above line to inject your values programmatically
[userContentController addUserScript:cookieScript];
WKWebViewConfiguration* webViewConfig = WKWebViewConfiguration.new;
webViewConfig.userContentController = userContentController;
WKWebView * webView = [[WKWebView alloc] initWithFrame:CGRectMake(/*set your values*/) configuration:webViewConfig];

设定Cookie的辅助方法:

1
2
3
4
5
6
7
8
9
10
11
12
- (NSString *)cookieString {
NSString *string = [NSString stringWithFormat:@"%@=%@;domain=%@;path=%@",
self.name,
self.value,
self.domain,
self.path ?: @"/"];

if (self.secure) {
string = [string stringByAppendingString:@";secure=true"];
}
return string;
}

因为Cookie注入无法跨域,此方法无法解决跨域请求的Cookie问题。

WKProcessPool同步方案解决跨域问题

苹果开发者文档对WKProcessPool的定义是:A WKProcessPool object represents a pool of Web Content process. 通过让所有WKWebView共享同一个WKProcessPool实例,可以实现多个 WKWebView之间共享Cookie(session Cookie and persistent Cookie)数据。

既然可以多个WKWebView共享一个WKProcessPool实例,那么是不是可以先访问跨域的URL,将Cookie注入,不就得到了一个有跨域Cookie的WKProcessPool实例吗?再用此实例去访问原始的Web页面,不就可以解决跨域访问的Cookie问题了吗? 经过实测,此方案是OK的。 样例如下:

可以创建一个单例管理WKProcessPool。如果Cookie更新了,更新WKProcessPool即可同步。

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
- (void)updateProcessPool {
_isUpdatePooling = YES;

if (_webView) {
_webView.navigationDelegate = nil;
[_webView removeFromSuperview];
_webView = nil;
}

if (_delegate && [_delegate respondsToSelector:@selector(willUpdateCookieWithPool)]) {
[_delegate willUpdateCookieWithPool];
}

//JS注入
NSString *jsStr = [self updateCookieScriptString];
//JS注入的domain(跨域注入会失败)
NSURL *url = [NSURL URLWithString:@"http://www.pingan.com.cn/"];
WKUserScript * cookieScript =
[[WKUserScript alloc] initWithSource:jsStr
injectionTime:WKUserScriptInjectionTimeAtDocumentStart
forMainFrameOnly:NO];
WKUserContentController* userContentController = WKUserContentController.new;
[userContentController addUserScript:cookieScript];
WKWebViewConfiguration* configuration = WKWebViewConfiguration.new;
configuration.userContentController = userContentController;
configuration.processPool = self.pool;

WKWebView *webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration];
[[UIApplication sharedApplication].keyWindow addSubview:webView];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:3];
webView.navigationDelegate = self;
[webView loadRequest:request];
_webView = webView;
}

#pragma mark - WKNavigationDelegate
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
LOG_INFO("WEB", @"didFinishNavigation :%@", navigation);
webView.navigationDelegate = nil;
[webView removeFromSuperview];

@synchronized(self) {
for (void(^block)(WKProcessPool *pool) in _handlers) {
block(_pool);
}
[_handlers removeAllObjects];
_isUpdatePooling = NO;
}

if (_delegate && [_delegate respondsToSelector:@selector(didUpdateCookieWithPool)]) {
[_delegate didUpdateCookieWithPool];
}

}

#pragma mark -
- (void)getCookieWithProcessPoolHandler:(void(^)(WKProcessPool *pool))handler {
if (_isUpdatePooling) {
[_handlers addObject:handler];
} else {
handler(_pool);
}
}

由于WKProcessPool只能在初始化时传入有效,所以调用有一点特殊:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@weakify(self);
PAWFWebProcessPoolManager *cookieManager = [PAWFWebProcessPoolManager sharedManager];
[cookieManager getCookieWithProcessPoolHandler:^(WKProcessPool * _Nonnull pool) {
@strongify(self);
self.webView = [self creatWebViewWithPool:pool];
[self.view addSubview:self.webView];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:self.URL];
[self.webView loadRequest:request];
}];

- (WKWebView *)creatWebViewWithPool:(WKProcessPool *)pool {
WKWebViewConfiguration* configuration = [[WKWebViewConfiguration alloc] init];
configuration.processPool = pool;
CGRect frame = CGRectMake(0, 0, kScreenWidth, CGRectGetHeight(self.contentView.frame) - kGrwonStatusBarHeight);
WKWebView *wkWebView = [[WKWebView alloc] initWithFrame:frame configuration:configuration];
wkWebView.navigationDelegate = self;
return wkWebView;
}

WKHTTPCookieStore同步cookie问题

WKHTTPCookieStore,存在一些奇怪的bug,完全无法使用,已知问题如下:

无法正常写入cookie的Bug:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
NSMutableDictionary *cookieProperties = [NSMutableDictionary dictionary];
[cookieProperties setObject:@"wifi_jsessionid" forKey:NSHTTPCookieName];
[cookieProperties setObject:jsessionid forKey:NSHTTPCookieValue];
[cookieProperties setObject:@".pingan.com.cn" forKey:NSHTTPCookieDomain];
[cookieProperties setObject:@"" forKey:NSHTTPCookieOriginURL];
[cookieProperties setObject:@"/" forKey:NSHTTPCookiePath];
[cookieProperties setObject:@"0" forKey:NSHTTPCookieVersion];
NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:cookieProperties];

WKWebsiteDataStore *store = [WKWebsiteDataStore defaultDataStore];
[store.httpCookieStore setCookie:cookie completionHandler:^{
[store.httpCookieStore getAllCookies:^(NSArray<NSHTTPCookie *> * _Nonnull cookies) {
//读取不到写入的cookie。
}];

}];

使用迂回方式解决写入问题:

1
2
3
4
5
6
7
8
9
10
11
12
//添加observer代理
[store.httpCookieStore addObserver:self];

//注意:网络不畅通时,存在不回调的情况。网络ok后才会调用。原因不明
- (void)cookiesDidChangeInCookieStore:(WKHTTPCookieStore *)cookieStore {
WKWebsiteDataStore *store = [WKWebsiteDataStore defaultDataStore];
[store.httpCookieStore getAllCookies:^(NSArray<NSHTTPCookie *> * _Nonnull cookies) {
LOG_INFO("WEB", @"cookiesDidChangeInCookieStore: %@", cookies);
//参考链接:https://forums.developer.apple.com/thread/97194
//此时才能看到Cookie被写入了
}];
}

cookie添加成功后,WKWebVie无法使用WKWebsiteDataStore中的cookie信息。详情可见:#140191

  • 自测添加跨域的cookie, Cookie添加成功后进行跨域访问,cookie无法生效。
  • 自测添加正常cookie也无法生效。

参考资料