这是一套安全、易用、可扩展的WebView JsApi框架,解决Android WebView中低版本中出现的安全漏洞问题,该JavaScript与Native通信的解决方案可以同时适用于Android和iOS平台。
一、背景
移动互联网时代的今天,原生APP的盛行,随着H5的进一步发展推广,移动Web APP呼声极高,也许将来的某一天Web会完全取代原生APP,这样是不无可能的。就当前的情况下,原生APP和移动Web各有各的优势,Web虽然具有实时版本更新的巨大优势,但是却不能像原生APP一样去访问系统各种API,因此出现了原生和Web混合的APP,也就是变动性比较大的功能模块用H5去实现,并由native程序提供给各种调用系统功能的接口给H5页面去调用。
在Android中我们使用WebView来展示一个网页,WebView中是有提供了一个addJavascriptInterface
接口以实现JavaScript和Java之间的交互,但是这个接口在Android 4.2一下的版本中存在严重的安全隐患,因为在Android 4.2一下版本的WebView存在严重的安全漏洞,JS中可以通过遍历window对象,找到存在“getClass”方法的对象,然后再通过反射的机制,得到Runtime对象,再然后…
Android的WebView中还存在各种各样的问题,内核内存泄露也是其中一个比较严重的问题,WebKit内核一旦出现了内存泄露,最有效的方法就只有杀掉进程了。
所以针对WebView所存在的上述几个问题,一个跨进程的JsApi框架就应运而生了。
二、框架应该具有的一些特性
1、易用性
框架提供的接口调用方法尽量简单,接口命名易于理解等;
2、通用性
框架的应该具有通用性,不应与具体业务相关;
3、稳定性
稳定性是程序设计里面的一个必备条件,框架的稳定性直接影响到使用框架的应用程序的稳定性,具有影响广泛的特性;
4、透明性
框架的具体实现对框架的使用者来说应该是透明的,使用者无需理解框架的内部原理和实现,只需按照文档描述使用对应接口即可完全掌握框架的使用;
三、JsApi框架用到的接口
1、shouldOverrideUrlLoading(WebView view, String url):
该接口是WebViewClient中的一个接口,当WebView中的页面里需要加载一个URL时会回调该方法,开发者可以通过返回true或false选择来决定是否中断改URL的加载,详情可以看SDK的文档;
2、onPageStarted(WebView view, String url, Bitmap favicon):
该接口是WebViewClient中的一个接口,当WebView开始加载一个URL时会回调该方法,,详情可以看SDK的文档;
3、onPageFinished(WebView view, String url):
该接口是WebViewClient中的一个接口,当WebView加载一个URL完成后会回调该方法,详情可以看SDK的文档;
4、onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result):
该接口是WebViewClient中的一个接口,当WebView中的页面中调用js的prompt函数时会回调改方法,详情可以看SDK的文档;
5、onConsoleMessage(ConsoleMessage consoleMessage):
该接口是WebViewClient中的一个接口,当WebView中的页面通过js调用console来输出日志时会回调改方法,详情可以看SDK的文档;
四、详细设计
本框架的设计上遵循严格的职能分层设计,每个层次之间不相关,层次之间的关系是服务提供和服务调用关系,逻辑严格分明,尽可能的达到松耦合。
框架中包括:
- JavaScript层
- JavaScript与Java通讯层
- 完整性校验及动态Proguard层
- JsApi调用Message转换层
- JsApi逻辑层
1、JavaScript层的设计
1)采用本地集成JavaScript代码的方式取代在H5中引用网络资源的方式
其原因是因为JavaScript语言的一些特性决定的,如果能获取function对象引用就可以轻易的获取读取内部的代码实现,并且全局变量可以轻易被替换,只能通过利用JavaScript中的闭包加以保护;
2)将第三方库和框架JavaScript代码分离
即使读取两个文件的速度会比读取合并成一个文件的速度快,但是考虑到框架的JavaScript代码需要做动态proguard处理的,动态proguard是会有一定耗时的,所以将其拆分出来,拆分出来后也有助于代码的阅读的后期的维护;
3)第三方JavaScript库代码也是采用本地集成并用闭包加以保护
将第三方库代码直接本地化,一方面防止被“偷梁换柱”,另一方面是为了减少对库版本兼容的支持问题;加上闭包的作用域保护是为了能够确保内部调用的JS库中的接口是安全且可信的,防止被中途截获或替换等;
4)接口
对外提供调用的接口
-
invoke,提供给页面调用JsApi的接口,其参数包括module、JsApi和callback;
-
invokeNoModule,提供给页面调用JsApi的接口,其参数包括JsApi和callback,该接口调用实际上是调用默认的module下的JsApi方法;
-
addEventListener,提供给页面注册监听事件的接口,其参数包括eventId和callback;
-
getEnvArg,提供给页面获取native程序传过来的环境变量、参数、配置等;
-
log,提供给页面输出log到native并输出到日志文件中;
私有接口
- _handleAckFromNative
处理从native过来的确认消息(ACK),以驱动框架中JavaScript层的运作;
- _handleMessageFromNative 处理从native回传的JsApi的callback或native发布到H5页面的事件(event);
5)JavaScript层状态图
2、JavaScript与Java通讯层设计
3、完整性校验及动态proguard
完整性校验是将JsApi调用或Event、Callback的原始数据通过SHA1生成摘要,用于JavaScript和Java两端进行校验,防止数据的伪造。
所谓的动态proguard其实就是字符串替换,每次加载新的页面后,在注入框架中的JS代码前,对JS代码某些特定字符串用一个随机串替换掉,防止JS代码被拦截、解析和伪造等。
动态proguard做如下处理
- 动态替换JS方法名或变量名;
- 动态插入参数或变量;(无用变量,用于打乱JS代码,防止被解析)
- 加入无用代码逻辑,防止关键逻辑被分析出来;
4、JsApi框架Java层设计
1)同一个WebView需要维护一个EnvironmentArg对象,使得JsApiFunc与具体调用场景无关。对应于WebView的EnvironmentArg对象用于存储该WebView上下文中的一些参数数据,跨WebView无法访问,同时可以通过这个EnvironmentArg对象的getOuterArgs()方法获取到一个顶级的EnvironmentArg对象,该对象用于存储跨WebView上下文的一些参数供各WebView访问;
2)需要弹dialog或需要调用startActivityForResult方法时,直接在Service里面是无法完成的,这时需要启动一个透明的代理Activity来创造一个Activity的上下文环境;
3)Java层简要流转逻辑
五、框架的使用
由于该框架的JsApi调用是支持跨进程的,并支持调用JsApi逻辑与WebView处在同一进程或不同进程两种模式,所以需要启动一个Service,该service与WebView处在不同的进程,添加的JsApi则在service创建的时候
1、Service
继承框架中的BaseJsApiCoreService类,并在其doAddJsApiFunc(ICallbackProxy callbackProxy)
方法中添加JsApiFunc
对象(非强制);
示例:
public class JsApiCoreServiceImpl extends BaseJsApiCoreService {
@Override
protected void doAddJsApiFunc(ICallbackProxy callbackProxy) {
addJsApiFunc(new JsApiFunc_Log());
addJsApiFunc(new JsApiFunc_ShowLoadingDialog(callbackProxy));
}
}
2、在Java中添加JsApi实现
1)实现JsApiFunc
实现一个JsApi只需要继承BaseJsApiFunc
,设置JsApi的id、module(可选)和funcName,并实现方法doInvoke即可;
示例:
public class JsApiFunc_Test extends BaseJsApiFunc {
private static final String TAG = JsApiFunc_Test.class.getSimpleName();
public JsApiFunc_Test() {
super(0, "ModuleTest", "doTest");
}
@Override
public boolean doInvoke(EnvironmentArgs args, JsInvokedJsApiMessage msg) {
Log.i(TAG, "doInvoke, just doTest.(module : %s, func : %s)", msg.getModule(), msg.getFunction());
return false;
}
}
2)添加JsApi对象到框架中
可以通过在1中所述的Service中调用addJsApiFunc来添加,或直接通过SecurityWebView中的addJsApiFunc方法添加;
3、在JavaScript中调用JsApi
在JavaScript中调用JsApi可以通过如下方式调用:
// 调用默认module下的JsApi
JSBridge.invokeNoModule('showLoadingDialog', {/*参数*/}, function(res) {
// 回调函数
alert("res.module : " + res.module + "\n\nres.func : " + res.func);
});
// 调用某个module下的JsApi
JSBridge.invoke('Module', 'doTest', {/*参数*/}, function(res) {
// 回调函数
alert("res.module : " + res.module + "\n\nres.func : " + res.func);
});
4、在JavaScript中注册事件监听
在JavaScript中注册事件监听可以通过如下方式调用:
// 注册事件监听
JSBridge.addEventListener('eventId', function() {
// 监听到时间处理逻辑
JSBridge.log('event published.');
});
5、在JavaScript中获取上下文相关参数
var value = JSBridge.getEnvArg('key');
JSBridge.log(value);
6、在JavaScript中log函数的使用
var key = 'k', value = 'v';
JSBridge.log('key : %s, value : %s', key, value);
六、实现中遇到的问题及解决方案
1、包含特殊字符的json串会使得在Java层和JavaScript层运算出来的SHA1摘要不等的问题:
原因:特殊字符通过JSON.stringify()函数后得到的是转义过的串,导致再进行SHA1算法时会出现运算出的摘要与原字符串不一致的;
解决方案:重写一个不转义特殊字符JSON.stringify()方法,使得字符串一致。
2、加载完一个页面onPageFinished接口被对调多次:
原因:因为框架中嵌入了多个iframe标签,每个iframe标签中对应的页面加载完毕时,onPageFinished会被回调一次,从而导致onPageFinished接口被多次回调;
解决方案:通过调用次数限定处理,在onPageStarted后,只有第一次onPageFinished调用才被认为是页面加载完毕,才做对应的逻辑处理;
3、如果采用的是onJsPrompt接口作为框架连接JavaScript和native程序的桥梁,偶尔会出现被卡死的状态:
原因:通过实验查出卡死的原因是因为从JavaScript中传过来的字符串数据过长,
解决方案:避免在字符串过长的情况下使用onJsPrompt接口,可以使用另外两个接口代替;
4、提高TargetApi level到20后,出现通过WebView.loadUrl(“javascript:xxx”)来运行JavaScript代码失效:
原因:高版本的WebView.loadUrl(““)不在对javascript这样的scheme生效,需要运行JavaScript代码时需使用WebView.evaluateJavascript(“javascript:xxx”)来代替;
解决方案:使用WebView.evaluateJavascript(“javascript:xxx”)代替WebView.loadUrl(“javascript:xxx”)来运行JavaScript代码;
5、初始化WebView的线程和调用loadUrl需要在同一线程中.
七、总结与展望:
该项目目前还没整理出来,整理出来后会上传到github上,大家一起推进该框架的更新。
- Java与JS通信需要带上一个token(时间相关);
- JsApi分两种类型: (1)正常调用; (2)阻塞调用(有UI);
- JsApi同个签名的可以选择同步或异步两种方式;(未做)
- 自定义伪协议;
- 模仿WebView.addJavaScriptInterface();(module + func,要使得无差别,需要包一层js的封装)
- JsApi的对象可以是单例或多例;(多例的JsApi需要hold住调用的WebView)
- 同一个WebView需要维护一个EnvironmentArg对象,是的JsApiFunc与具体调用场景无关;
- 动态proguard: (1)动态替换JS方法名或变量名; (2)动态插入参数或变量;(无用变量,用于打乱JS代码,防止被解析) (3)加入无用代码逻辑,防止关键逻辑被分析出来;
- 每一次调用都更新token(部分更新),token是一个集合,有java层决定下一次调用用哪一个token;(这里需要加入加密技术);
- URL生成摘要,并进行摘要的校验;
- 自动检测当前客户端支持的模式,自动测试性能和耗时,择优;