1. 首页
  2. 科技部落

iOS WebView采坑篇之同步登录态总结

前言

随着业务上对产品体验、迭代速度、效果验证等方面的诉求越来越强烈,为了使功能需求能够快速上线,降低及不依赖于客户端发版,项目中Native页转H5页面比例越来越高。这就牵扯到一个问题,如何把登录状态同步给H5页面呢?总不能打开网页时再从网页中登录一次系统吧, 为了用户更好的体验,原生与H5页面登录状态的同步是必须的。

同步登录状态原理:

1. 判断是否需要登录态

现实场景中并非所有的H5页面都需要登录态,怎么区分哪些H5页面是需要登录态的呢?
我们用needAuth参数来标识,URL的query参数里如果有needAuth=true的标识,代表此页面必须要校验登录态,如果APP未登录,让用户去登录。反之没有此标识可以不校验登录态,直接打开URL。

2. 设置登录态

我们项目中采用业界广泛使用token的身份验证方式,就是用户在APP登录成功后,会通过客户端token获取到H5的token,并把获取到的H5 token放在本地Cookie中,设置了Cookie后,打开H5页面发起的ajax请求时会自动带上 Cookie,H5或者服务端根据Cookie里的token参数识别用户信息。在用户退出登录APP后,清除Cookie及本地存储的用户信息。

具体代码实现

这里说下H5页面加载到Native中,必须用容器来承载。iOS存在的H5容器主要包括UIWebView和WKWebView。

UIWebView设置Cookie:

如果用UIWebView去设置Cookie是非常简单的,因为UIWebView对cookie是通过NSHTTPCookieStorage来统一处理的,服务端响应时写入,UIWebView 发起请求会自动带上 NSHTTPCookieStorage 中的 Cookie,所以UIWebView进行跨域请求是没有任何问题的,只要NSHTTPCookieStorage有符合条件的Cookie信息即可。

为什么不用UIWebView,而用WKWebView呢?

因为UIWebView内存占用量巨大、内存泄漏实在太过严重、API太老等问题,而WKWebView相比于UIWebView:

  • 多进程,在app的主进程之外执行: WKWebView为多进程组件,也意味着会从App内存中分离内存到单独的进程(Network Process and Rendring Process)中。当内存超过了系统分配给WKWebView的内存时候,会导致WKWebView浏览器崩溃白屏,但是App不会Crash。(app会收到系统通知,并且尝试去重新加载页面)
  • 异步执行处理JavaScript: WKWebView是异步处理app原生代码与JavaScript之间的通信,因此普遍上执行速度会更快。且JavaScript API调用原生(native)方法不会阻塞线程,而且它的回调代码块总是在主线程中运行。官方文档:Evaluates a JavaScript string.The method sends the result of the script evaluation (or an error) to the completion handler. The completion handler always runs on the main thread.
  • 允许JavaScript的Nitro库加载并使用: WKWebView使用和手机Safari浏览器一样的Nitro JavaScript引擎,相比于UIWebView的JavaScript引擎有了非常重要的性能提升。
  • 支持对错误的自签名安全证书和证书进行身份验证: 在WKWebView中,WKNavigationDelegate中提供了一个权限认证的代理方法,允许绕过安全证书中的错误。
  • 新增一些属性,对之前类进行更加详细拆分,使用更加方便等等。
  • 最重要的一点是从iOS13 开始苹果将 UIWebView 列为过期API,不再维护,所以WKWebView代替UIWebView是势在必行的。

WKWebView设置Cookie问题:

但是WKWebView设置Cookie相对于UIWebView来说不是很方便。 WKWebView Cookie 问题在于WKWebView 发起的请求不会自动带上存储于 NSHTTPCookieStorage 容器中的Cookie。因为WKWebView 请求已不在app进程中发起和响应处理,而是在专门的web进程中处理,所以WKWebView发起的请求无法直接从NSHTTPCookieStorage取到Cookie。

Cookie问题解决方案:

针对这个问题,我们的解决方法是客户端新建TokenCookieManager去手动管理cookie的存储。在加载 URL 之前,检查 WKWebView的WKHTTPCookieStore 中的cookie是否存在token。如果不存在,获取服务响应的cookie后,直接写入 WKWebView的cookie 中,再去加载URL,这样在处理正式的请求时,就会保证H5可以在cookie中获取到token。

流程如下: 

iOS WebView采坑篇之同步登录态总结

syncCookies流程:  

iOS WebView采坑篇之同步登录态总结

这里我们在syncCookies时添加了队列管理,保证同时对cookie并发操作时,每次sync都能执行完毕并回调。
苹果在iOS11之前没有专门的API用于Cookie操作, 在iOS 11之后,苹果添加了WKHTTPCookieStore权限,使可以完全访问Web视图的cookie存储。需要注意这个接口是异步的,我们需要等待WKWebView异步设置cookie完成后才进行下一步操作。

WK_EXTERN API_AVAILABLE(macos(10.13), ios(11.0))
@interface WKHTTPCookieStore : NSObject

- (instancetype)init NS_UNAVAILABLE;

/*! @abstract Fetches all stored cookies.
@param completionHandler A block to invoke with the fetched cookies.
*/
- (void)getAllCookies:(void (^)(NSArray<NSHTTPCookie *> *))completionHandler;

/*! @abstract Set a cookie.
@param cookie The cookie to set.
@param completionHandler A block to invoke once the cookie has been stored.
*/
- (void)setCookie:(NSHTTPCookie *)cookie completionHandler:(nullable void (^)(void))completionHandler;

/*! @abstract Delete the specified cookie.
@param completionHandler A block to invoke once the cookie has been deleted.
*/
- (void)deleteCookie:(NSHTTPCookie *)cookie completionHandler:(nullable void (^)(void))completionHandler;

在iOS 11中 WKHTTPCookieStore setcookie及deleteCookie 有时候会出现 completionHandler不回调。苹果在iOS 12 Beta中进行了修复,所以加了超时逻辑,防止completionHandler不回调。

 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self apiSetCookiesHandler];
});

iOS11 以前的系统版本,设置Cookie就没有那么直接,可以通过key-Value构造一个Cookie,运行-evaluateJavaScript:completionHandler:访问JavaScript 通过document.cookie设置Cookie,解决请求 Cookie 问题。

[self.wkWebView.configuration.userContentController removeAllUserScripts];
NSMutableSet *scriptSet = [[NSMutableSet alloc] init];
for (NSHTTPCookie *cookie in self.cookies) {
NSString *str = [NSString stringWithFormat:@"document.cookie='%@=%@;domain=%@;path=/'", cookie.name, cookie.value, cookie.domain];
// maxAge 单位为秒
if(cookie.expiresDate > [NSDate date]) {
str = [NSString stringWithFormat:@"document.cookie='%@=%@;domain=%@;path=/;max-age=%f;'", cookie.name, cookie.value, cookie.domain, cookie.expiresDate.timeIntervalSinceNow];
}
[scriptSet addObject:str];
}
NSString *scriptString = [scriptSet.allObjects componentsJoinedByString:@"\n"];
WKUserScript *userScript = [[WKUserScript alloc] initWithSource:scriptString injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
[self.wkWebView.configuration.userContentController addUserScript:userScript];

NSString *URLStr = self.loadURLString.length > 0 ? self.loadURLString : @"https://loanweb.ppdai.com";
NSURL *URL = [NSURL URLWithString:URLStr];
NSString *baseURLString = [NSString stringWithFormat:@"%@://%@", URL.scheme, URL.host];
[self.wkWebView loadHTMLString:@"" baseURL:[NSURL URLWithString:baseURLString]];

生成cookie时 需要注意 cookie有四个关键的标识:value(键值对)、expires(过期日期)、domain(域)、path(路径),如果有一个标识不一样,它就是一个新的cookie,在iOS中如果cookie只指定value,其他会设置为默认值,会导致因为同源策略而请求带不上cookie。

退出APP清除信息:

用户退出登录后需要把缓存的用户信息及cookie清除,防止APP出错。但由于WKWebView内部默认使用一套缓存机制,开发者可以操作的权限会有限制,特别是IOS8版本,根本没法操作。在IOS 9 之后,系统提供了缓存管理的接口WKWebsiteDataStore

WK_EXTERN API_AVAILABLE(macos(10.11), ios(9.0))
@interface WKWebsiteDataStore : NSObject <NSSecureCoding>

/*! @abstract Removes all website data of the given types that has been modified since the given date.
@param dataTypes The website data types that should be removed.
@param date A date. All website data modified after this date will be removed.
@param completionHandler A block to invoke when the website data has been removed.
*/
- (void)removeDataOfTypes:(NSSet<NSString *> *)dataTypes modifiedSince:(NSDate *)date completionHandler:(void (^)(void))completionHandler;

至于IOS8系统,可通过删除文件来解决了,一般WKWebView的缓存数据会存储在这个目录里,可通过删除该目录来实现清理缓存。

~/Library/Caches/BundleID/WebKit/

项目中遇到 native-h5-native 这样的业务链路,这种业务链路中h5的节点通过ajax请求产生了一个新的cookie需要带到下一步的 native 中, 但cookie同步是单向,只有app侧的NSHTTPCookieStorage将cookie同步到WKWebView维护的cookieStorage,WKWebView的cookie无法同步到NSHTTPCookieStorage中,那怎么保证WKWebView的cookie也同步到App的NSHTTPCookieStorage?

在iOS11之后,苹果提供一个接口用于配置observer监听cookie的改变:

WK_EXTERN API_AVAILABLE(macosx(10.13), ios(11.0))
@interface WKHTTPCookieStore : NSObject
/*! @abstract Adds a WKHTTPCookieStoreObserver object with the cookie store.
@param observer The observer object to add.
@discussion The observer is not retained by the receiver. It is your responsibility
to unregister the observer before it becomes invalid.
*/
- (void)addObserver:(id<WKHTTPCookieStoreObserver>)observer;

/*! @abstract Removes a WKHTTPCookieStoreObserver object from the cookie store.
@param observer The observer to remove.
*/
1. (void)removeObserver:(id<WKHTTPCookieStoreObserver>)observer;
@end

在iOS11 之前解决方案:WKWebView初始化时注册一个JS函数,h5有cookie操作时通过Hook这个JS函数将cookie回传到本地进行同步。 大致流程如下图: 

iOS WebView采坑篇之同步登录态总结

其他问题

– 跳转到第三方APP:

项目中需要跳转到第三方APP,发现WKWebView 限制了 WEB 页面打开三方 APP 的能力,必须在此代理方法中拦截,

/*! @abstract Decides whether to allow or cancel a navigation.
@param webView The web view invoking the delegate method.
@param navigationAction Descriptive information about the action
triggering the navigation request.
@param decisionHandler The decision handler to call to allow or cancel the
navigation. The argument is one of the constants of the enumerated type WKNavigationActionPolicy.
@discussion If you do not implement this method, the web view will load the request or, if appropriate, forward it to another application.
*/
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;

判断 URL 中的 Scheme 或 host,然后通过 [[UIApplication sharedApplication] openURL:] 方法打开。需要注意的是,对于三方APP,通过 Scheme 来判断即可,如高德地图的 scheme 是 iosamaps。但对于 iOS 部分自带 App,则需要通过 URL host 判断,如 AppStore 的 host 是 http://itunes.apple.com

本文来自拍码场,经授权后发布,本文观点不代表信也智慧金融研究院立场,转载请联系原作者。