40- WKWebView项目实践分享(五)- WKWebView如何加Cookie

系列文章:

在iOS11以下的版本中,WKWebView并没有类似UIWebView中NSHTTPCookieStorage这样的可以直接设置和存储Cookie的官方类。但是在各个App的实际开发过程中,给HTML5后台传入Cookie进行身份校验等属于最基本的操作,正是因为不能方便设置Cookie的原因,在本文发布的时候,很多大厂App的核心web业务并没有使用WKWebView,仍然是UIWebView。iOS11开始,苹果意识到了这个问题,做了妥协,新增WKHTTPCookieStorage来帮助开发者解决Cookie问题,虽然方便了,但是API仍然不稳定,比如iOS11.3的时候就出现了一些变动。

针对WKWebView设置Cookie的问题,为什么在iOS8-iOS11长达三年的时间里,苹果没有理会呢?CSDN作者请叫我马老师的一篇文章中,提出了猜想,苹果是想让服务端完全控制Cookie,毕竟客户端操作Cookie是有一定安全风险的。WKWebView设计出来,对安全性就有比较高的要求,比如说对跨域的限制。

客户端不传Cookie之后,服务端怎么确认用户的身份信息呢?其中的一种方式,我们可以在首次请求的时候,将用户信息身份信息通过POST请求的方式放到requst中。服务端接收到首次请求,判断用户身份信息。如果身份信息有效,在之后的请求中,服务器会主动设置Cookie保证后续的请求身份验证是通过的。如果身份信息无效,则跳转到登录页面。 这种方式就是Cookie完全交给服务器管理了。

但是,实际开发中,我们还是免不了客户端设置Cookie。下边说说在WKWebView中设置Cookie的方案。

WKWebView中设置Cookie的方案

在UIWebView中,可以通过NSHttpCookieStorage来管理Cookie,对Cookie进行增删改查。当把Cookie添加到NSHttpCookieStorage中后,webView在发起请求的时候会自动从NSHttpCookieStorage中取出Cookie添加到request Header中,并且这个操作是零延迟的。也就是说,你第一行代码添加Cookie到NSHttpCookieStorage中,第二行代码loadRequest:,发起的请求中是带有你设置的Cookie的。
关于零延迟这一点非常重要,因为你设置了Cookie,但是请求发起之后,request Header拿不到你设置的Cookie,导致服务器找不到Cookie,从而身份校验不通过。

NSHttpCookieStorage对WKWebView而言,只能管理Cookie,但是没有了零延迟自动同步到request Header这一关键特性。所以我们只能另找出路。正式因为修正这个问题,我们需要找对应方案适配不同的版本

截至本文发布时候,需要适配iOS版本分为三个范围,分别是iOS8iOS11.0、iOS11.0iOS11.2、iOS11.3,iOS11.3以后的版本因为有了官方APIWKHTTPCookieStorage的加持,可以直接套用iOS11.3的方案。 下边看方案的具体实施:

Cookie版本适配方案

下边就针对每个适配版本来说一下具体操作方案。

适配iOS8~iOS11.0

首先这一步适配需要关注的地方有两处:

  • 第一处是我们每次调用loadRequest:方法的时候,需要加Cookie。
  • 另一处就是loadRequest:成功之后,webView的后续页面跳转的时候,也需要加Cookie。

第一处,我们的解决思路是使用NSHttpCookieStorage管理Cookie。然后在每次调用loadRequest:方法的时候,我们把Cookie手动写入到request Header中。具体代码和步骤如下:

// 1. 清除NSHTTPCookieStorage中存储的Cookies,避免重复Cookie
NSHTTPCookieStorage *cookieJar = [NSHTTPCookieStorage sharedHTTPCookieStorage];
for (NSHTTPCookie *cookie in cookieJar.cookies) {
    if ([cookie.comment isEqualToString:WebCookieComment]) {
        [cookieJar deleteCookie:cookie];
    }
}

// 2. 重新设置Cookies
[[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
[[NSUserDefaults standardUserDefaults] synchronize];

// 3. 向Http Header中设置Cookie    
 NSDictionary *dict = [NSHTTPCookie requestHeaderFieldsWithCookies:[NSHTTPCookieStorage sharedHTTPCookieStorage].cookies];
 request.allHTTPHeaderFields = dict.copy;

// 4. 加载请求
[self.wkwebView loadRequest: request];

第二处后续webView跳转中加Cookie,我们的解决思路是把Cookie添加到HTML中的document.Cookie,代码如下:

//1. 添加自定义的cookie,后续请求Ajax页面用到
WKUserScript *newCookieScript = [[WKUserScript alloc] initWithSource:[WebViewCookieTool cookieJavaScriptString] injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
[contentController addUserScript:newCookieScript];
webConfiguration.userContentController = contentController;


//2. 获取Cookie的JS代码
+ (NSString *)cookieJavaScriptString
    NSMutableString *script = [NSMutableString string];
    for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]) {
    if ([cookie.value rangeOfString:@"'"].location != NSNotFound) {
         continue;
    }
    [script appendFormat:@"document.cookie='%@'; \n", [self getCookieJSFromatString:cookie]];
}
    
    
+ (NSString *)getCookieJSFromatString:(NSHTTPCookie *)cookie
{
    NSString *cookieStr = [NSString stringWithFormat:@"%@=%@;domain=%@;path=%@",
                           cookie.name,
                           cookie.value,
                           cookie.domain,
                           cookie.path ?: @“/"];
    return cookieStr;
}

这种方案出现的问题

这种方案基本解决了iOS8-iOS10设置Cookie的问题,但是在实测中,发现将Cookie添加到request Header中会导致另一个问题出现。那就是如果在webView带着Cookie发起请求之后,HTML服务器如果也想设置一些Cookie到header中,是不能正常设置的或者说添加不上,现象就是网页无法正常显示。针对这个问题,想到了两个解决方案:

解决服务器无法在Header中正常设置Cookie

所以,我项目里有一个区分是不是本公司请求的静态方法:

static inline BOOL checkURLIsNeedCookies(NSURL *url) {
    if ([url.host hasSuffix:@".myCompany.com") {
        return YES;
    }
    return NO;
}

适配iOS11.0~iOS11.2

iOS11.0之后的适配就爽啦,苹果在iOS11.0推出了WKHTTPCookieStore,这个类对WKWebView的效果和NSHttpCookieStorage对UIWebView的效果一样,并且也是零延迟。在请求发起的时候,我们只需要将Cookie设置到WKHTTPCookieStore就可以了。代码如下:

// 1. 设置Cookie
WKWebsiteDataStore *store = [WKWebsiteDataStore defaultDataStore];
[store.httpCookieStore setCookie:cookie completionHandler:^{
NSLog(@“cookie添加成功”);
}];

// 2. 加载请求
[self.wkwebView loadRequest: request];

适配iOS11.3

前几天升级iOS11.3之后发现一个问题。具体出错的位置是需要上传Cookie的HTML页面,WKHTTPCookieStore第一次设置Cookie之后,发起请求,Cookie不能同步到request Header中。此时,webView重新请求页面,WKHTTPCookieStore的作用恢复正常,之后请求其他的webView也不会出现Cookie丢失的问题。 总结就是在iOS11.3中,WKHTTPCookieStore设置Cookie之后,第一次发起请求Cookie会丢失,第二次及以后的请求的Cookie没有问题。

解决方式是,我们在执行loadRequest之前主动设置一次Cookie。我这里是在didFinishLaunchOption:中设置的。设置完之后,就没有问题了。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 
{
   WKWebsiteDataStore *store = [WKWebsiteDataStore defaultDataStore];
    [store.httpCookieStore setCookie:cookie completionHandler:^{
        NSLog(@“cookie添加成功”);
    }];
}

好了,关于WKWebView的Cookie设置就到此为止。

共享Web Content Process

我们之前说过,一个WKWebView创建出来之后,对应一个Web Content Process。这样就保证每个WKWebView的缓存、Cookie等信息是隔离的。但是这样会导致一个问题就是,当网页中出现<a>标签和target=_blank的时候,会好像浏览器那样新开一个tab页面,此时新开的webView不共享不到之前webView的Cookie的,导致新开的webView不能正常显示。 处理的方式是,所有的WKWebView共享同一个processPool。 下面有两种设置方式:

 1’:   
self.processPool = [[WKProcessPool alloc] init];
WKWebViewConfiguration *configuration1 = [[WKWebViewConfiguration alloc] init];
configuration1.processPool = self.processPool;
WKWebView *webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration1];


2’:
- (WKProcessPool *)wkProcessPool
{
    static dispatch_once_t once;
    static WKProcessPool * __singleton__;
    dispatch_once( &once, ^{
        __singleton__ = [[WKProcessPool alloc] init];
        
    });
    return __singleton__;
}

 webConfiguration.processPool = self.wkProcessPool; 
 WKWebView *webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration1];

因为设置了单利的缘故,我们在HTML页面退出的时候,因为销毁单利。否则会导致一些问题。在《42- WKWebView项目实践分享(六) - 补充: 实践中的坑》提到。

参考

交流


希望能和大家交流技术
Blog:http://www.lilongcnc.cc


推荐阅读更多精彩内容