WKWebView拦截实现探讨
背景
苹果更新邮件
Updating Apps that Use Web Views December 23, 2019 If your app still embeds web content using the deprecated UIWebView API, we strongly encourage you to update to WKWebView as soon as possible for improved security and reliability. WKWebView ensures that compromised web content doesn’t affect the rest of an app by limiting web processing to the app’s web view. And it’s supported in iOS and macOS, and by Mac Catalyst. The App Store will no longer accept new apps using UIWebView as of April 2020 and app updates using UIWebView as of December 2020.
项目中UIWebView使用场景
- jssdk使用 h5调用方法后JSSDK新建iframe节点,webview监听事件进行处理
- 签名授权 webview拦截带sign_suffix后缀的请求后,客户端去请求授权
- 加载离线资源 使用系统的api NSURLProtocol在上层拦截webview中的离线请求,客户端进行离线处理
iOS拦截url加载离线实现流程图
iOS端加载离线就是如何将离线url拦截并返回数据的过程
实现
NSURLProtocol
Don't instantiate an NSURLProtocol subclass directly. Instead, create subclasses for any custom protocols or URL schemes that your app supports. When a download starts, the system creates the appropriate protocol object to handle the corresponding URL request. You define your protocol class and call the registerClass: class method during your app’s launch time so that the system is aware of your protocol.
基本使用
- canInitWithRequest:系统询问是否拦截request,通常是匹配url是否满足特定要求。比如标准版中的几种拦截方式:
- 网页静态资源拦截规则:host以zuolin.com结尾,path以.js .css .png .gif .webp结尾
- 本地图片拦截规则:scheme == zl,host == resourceid
- 网络图片拦截规则:host以zuolin.com结尾,path包含/image
- 离线包拦截规则:host以zuolin.com结尾,path包含/nar
- initWithRequest:自定义urlProtocol的初始化方法
- startLoading:request开始加载,在这里处理缓存逻辑,如果有缓存返回data,没有缓存由native发起请求返回data
- stopLoading:request结束加载
缺陷
- 不像UIWebView的加载是在app的进程,WKWebView是独立的进程,如果不手动注册scheme,app根本收不到urlProtocol的相应方法
- 如果手动注册scheme后,wkWebView通过IPC与app所在进程通信,由于性能问题,会丢弃request中的body及bodyStream字段,H5中所有的post请求会失效
workaround
- H5侧将post请求参数放在header中,urlProtocol拦截后取header中的参数拼接在body中,由客户端完成请求
- wkWebView注入js hook ajax post请求后由客户端完成请求
- ①中记录xhrid
- ②中根据xhrid找到对应的xhr
WKUrlSchemeHandler
A protocol for loading resources with URL schemes that WebKit doesn't know how to handle. iOS11后开放的API,只拦截自定义scheme的url,常用的http、https链接拦截不了
基本使用
- customHandler:实现WKUrlSchemeHandler的实例,设置为配置项后,wkWebView中加载customScheme的request会走customHandler的处理
let config = WKWebViewConfiguration()
config.setURLSchemeHandler(HttpProxyHandler(), forURLScheme: "customScheme")
- startUrlSchemeTask:开始加载自定义scheme的request,在这里处理缓存逻辑,有缓存返回data,无缓存native请求后返回data
- stopUrlSchemeTask:结束加载request
H5支撑
需要H5侧统一所有离线资源的url,如果客户端使用的WKWebView(客户端在UA里面加标识),将url的scheme改为协定的customScheme,交由客户端处理
WKURLSchemeHandler拦截方案评估
拦截流程
- 替换根节点(html)的scheme为协定的customScheme,交由客户端处理
- 后续所有节点的request由于同源(相对路径、相对协议),也会带上协定的customScheme,也会被客户端拦截处理
缺陷
- xhr的post请求被拦截后,iOS13以下的设备request会丢失body,客户端拦截请求后反而会失败
解决方案
- HTML里面的资源文件全部用://user/a.png这种相对的写法。这样 http 和 自定义协议都可以用
- HTML里面的xhr post请求使用绝对路径,客户端不拦截处理,修改后端的 Response 头,使用 Cross-Origin Resource Sharing (CORS) 技术
代理XHR拦截方案评估
实现流程
通过代理xhr对象监听h5里面xhr的post请求,监听到的请求交由客户端去处理,代理xhr对象拿到数据后会调用xhr的相应事件完成请求
实现难点
- 代理xhr对象的编写(网上参考ajaxhook.js)
- 代理xhr对象数据多样性的传递及H5侧的解析
h5的xhr请求是否只能解析content-type:application/json的数据?
- 代理xhr对象与native的交互
1、2由h5端实现,3由客户端实现
Cookie的使用及问题
使用
- UIWebView
-
- 客户端设置
由于uiwebview中的所有请求都会带上NSHttpCookieStorage中的cookies,所以可以使用NSHttpCookieStorage直接设置cookie
- 后台设置
接口返回的response header中返回set-cookie,系统会自动同步进NSHttpCookieStorage
- WKWebView
wkwebview有自己独立的进程,请求时不会使用NSHttpCookieStorage中的cookie,并且wkwebview同步cookie进NSHttpCookieStorage的时机有延迟。
wkwebview中的cookie注入有三种方式:
- NSURLRequest header中手动加入cookie
- WKWebView中通过js注入cookie
- iOS11以后,通过WKWebSiteStore.httpCookieStorage设置cookie,用法和NSHttpCookieStorage类似
- 项目中WKWebView中cookie的管理和使用
- 用户登录后后台返回cookie,wkwebview创建后第一次请求时,会在request中加入cookie
- decidePolicyForAction:方法中每次跳转页面时会同步cookie
- 同步NSHttpCookieStorage中的cookie
- 同步登录时后台返回的cookie
- decidePolicyForResponse:方法中将response header里面的cookie同步至NSHttpCookieStorage,为后续页面cookie的同步作准备
问题
A页面中调用了某个接口,后台设置了cookieA,跳转B页面时拿不到cookieA
- 项目中使用
现在用户登录后会返回相应的cookie,因为某些环境未如期返回cookie,造成H5应用首页中调用logonBySignature设置cookie后,后续跳转到的页面拿不到cookie,引起调用异常。
wkwebview的cookie只能客户端手动注入,其相应的代理方法也只能监听主页面(mainFrame)的回调,业务接口中设置的cookie拿不到。此问题需前后端进行优化。
场景:电商跳同一订单接口报未授权
- 旧的逻辑
- 新逻辑(改后逻辑)
本质是后台set-cookie,cookie跨域,在客户端不干预的前提下,跨域的页面拿不到cookie,所以应该尽量避免302塞cookie的场景
- 第三方网页
- oauth
针对oauth授权的网页,wkwebview可在decidePolicyForResponse代理方法中拿到response,从而获取cookie,注入后实现后续页面跳转
- SSO
SSO授权的网页,一般token的产生在一个比较靠前的时间点,可以提前注入
方案确定
WKUrlSchemeHandler
针对post请求body丢失的缺陷,H5端由于业务复杂度无法改成绝对路径。
是否可代理xhr post请求,由客户端处理?
NSURLProtocol
通过代理xhr实现post请求拦截,对H5端无侵入。
目前H5侧仅仅支持json数据解析,如果protocol拦截第三方h5 post请求,需返回相应数据类型。
result
以上两种方案对比后,由于URLSchemeHandler的缺陷,决定使用xhr代理处理h5侧的post请求,使用urlProtocol拦截离线资源(get)请求。
waitToDo
- h5侧编写代理xhr的js
- 客户端编写与代理xhr通信的js
- h5写接口测试页面,后台写测试接口(接口尽量涉及全数据类型),客户端做拦截回调测试
代理xhr小结
能做什么
- 拦截xhr的post请求
- 拦截表单里面xhr的post请求
其他请求方式,如果wkwebview中注册了http、https,其post请求的body仍然会丢失
native交互
- 代理xhr拦截H5 post请求发送给native
window.bridge.call("xhrPOST", params);
/*
xhrPOST是native注册的方法,会引起native回调
params 是此次请求所带的参数(bridge只能传递基本数据类型,对象需做相应处理)
{
data;//请求参数
method;//请求方式
header;//请求header,{}
url;//请求地址
xhrId;//请求id
}
*/
- native请求成功数据通过代理xhr返回给H5
[self.webview evaluateJavaScript:[NSString stringWithFormat:@"window.bridge.receiveMessage(%@);",messageString] completionHandler:nil];
/*
receiveMessage是js bridge中注册的方法,会引起js回调
messageString是数据的json字符串,包括
{
xhrId;//请求id
statusCode;//请求状态码
responseText;//请求数据,string
resultType;//数据类型,1表示文本 2表示xml、html 3表示字节流
responseHeaders;// {}
error;//错误描述
}
*/
实现
native数据返回
/*
常见的媒体格式类型如下:
text/html : HTML格式
text/plain :纯文本格式
text/xml : XML格式
image/gif :gif图片格式
image/jpeg :jpg图片格式
image/png:png图片格式
以application开头的媒体格式类型:
application/xhtml+xml :XHTML格式
application/xml: XML数据格式
application/atom+xml :Atom XML聚合格式
application/json: JSON数据格式
application/pdf:pdf格式
application/msword : Word文档格式
application/octet-stream : 二进制流数据(如常见的文件下载)
application/x-www-form-urlencoded : <form encType=””>中默认的encType,form表单数据被编码为key/value格式发送到服务器(表单默认的提交数据的格式)
另外一种常见的媒体格式是上传文件之时使用的:
multipart/form-data : 需要在表单中进行文件上传时,就需要使用该格式
*/
根据httpResponse里面的content-type判断:
- text/plain、application/json,处理为文本,resultType == 1
- text/html、text/xml、application/xhtml+xml、application/xml、application/atom+xml,处理为文本,resultType == 1
- 剩余类型如果data能成功编码为string,当作文本处理,resultType == 1;否则data转为十六进制字符串,resultType == 3
resultType=3时,需将hexString转为字节流,赋值给xhr.response
xhr请求的参数传递
- 基本数据类型(int、string、array、dictionary)时,直接传递,webkit会做相应数据类型转换
- form表单中上传文件时,参数中会新增files数组,对应File会转为字节数组传给native
// files 为存储文件的数组
/* {
filename:fileName
type:mimeType
name:name
file:[uint8]
}*/
其他类型未处理,如ArrayBuffer、Blob
native上传body拼接
// 拼接httpBody
/* 分为2部分
{
key:value;//非文件部分
files:{//文件部分
filename:fileName
type:mimeType
name:name
file:[uint8]
}
}
*/
//示例
--boundraryidString
Content-Dispostion: form-data; name="name"; filename="filename"
Content-Type: "MIMETYPE"
文件部分
--boundraryidString
Content-Dispostion: form-data; name="key"
非文件部分参数值
--boundraryidString--
数据转换
Blob -> NSData
NSData -> Blob
以上为项目中所使用的的两种数据处理方式,可逆向
具体使用
EHWKWebView的创建
- 直接创建
用法和EHWebView使用一致
- (EHWKWebView *)webView
{
if (!_webView)
{
_webView = [[EHWKWebView alloc] initWithURLString:_URLString];
_webView.configuration.allowsInlineMediaPlayback = YES;//防止自动全屏播放
_webView.configuration.mediaTypesRequiringUserActionForPlayback = NO;
_webView.supportJSInterface = YES;
_webView.supportEmptyBack = YES;
_webView.scrollView.bounces = YES;
_webView.backgroundColor = WHITE_COLOR;
_webView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
_webView.navigationDelegate = self;
_webView.viewController = self;
}
return _webView;
}
- 通过EHWebViewCreatorManager创建
- (EHWKWebView *)webView {
if (!_webView) {
_webView = [[EHWebViewCreatorManager manager] getWKWebView];
_webView.originalURLString = _URLString;
_webView.viewController = self;
_webView.navigationDelegate = self;
}
return _webView;
}
EHWebViewCreatorManager 中包含是否预创建webview的策略,其创建只是赋值了一些基础属性,业务属性如urlString、viewController、navigationDelegate等留给外部处理
EHWebView的使用场景
需要使用JSSDK能力的网页使用EHWKWebView,其他场景如页面展示使用WKWebView即可
Q&A
如何切换uiWebView,wkWebView ?
如何使用提前创建的uiWebView或者wkWebView ?
wkWebView是怎样加cookie的 ?
- 第一次加载时loadRequest中在request的header里面设置cookie
- 后续页面内的跳转在wkwebview:decidePolicyForAction:代理方法中通过注入脚本的方式加cookie
wkWebView如何注册scheme的 ?
- 获取私有类型
cls = [[[WKWebView new] valueForKey:@"browsingContextController"] class]
- 获取私有方法
sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:")
sel = NSSelectorFromString(@"unregisterSchemeForCustomProtocol:")
- 注册自定义scheme
if ([(id)cls respondsToSelector:sel]) {
// 放弃编辑器警告
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[(id)cls performSelector:sel withObject:scheme];
#pragma clang diagnostic pop
}
wkWebView如何拦截post请求的 ?
通过代理xhr拦截H5页面中的post请求,拦截后交由客户端处理,客户端请求完将数据返回给H5页面,具体操作流程可见代理xhr实现
测试
测试范围
常规上是ios客户端的所有H5应用,但限制于时间和人力,目前仅仅测试核心的H5模块,包括
- 服务联盟
- 云打印
- 工位预定
- 品质核查
- 物业巡检
前面3个属于常规的H5应用,后面2个属于能离线的H5应用(弱网、断网情况下能正常显示页面)
测试标准
使用wkwebview加载的H5页面显示、跳转、请求无异常。
H5离线应用相对于常规应用,在有网条件下验证无误的情况下,还需在断网环境下检验页面是否能正常显示。
一般H5应用的流程能正常走通则说明没问题。
也可以使用两台测试机对比,一台是wkwebview加载显示,另一台是旧的uiwebview加载显示,如果wkwebview的结果和uiwebview一样,则说明没问题。
测试方法
从我的->设置->开发者选项->WKWebView测试->使用UIWebView/WKWebView,切换为WKWebView后,回到广场选择相应的H5应用进入即可开始验证。