在现在安卓应用原生开发中,为了追求开发的效率以及移植的便利性,使用WebView作为业务内容展示与交互的主要载体是个不错的折中方案。那么在 这种Hybrid(混合式) App中,难免就会遇到页面JS需要与Java相互调用,调用Java方法去做那部分网页JS不能完成的功能。

网上的方法可以告诉我们这个时候我们可以使用addjavascriptInterface来注入原生接口到 JS中,但是在安卓4.2以下的系统中,这种方案却我们的应用带来了很大的安全风险。攻击者如果在页面执行一些非法的JS(诱导用户打开一些钓鱼网站以进 入风险页面),极有可能反弹拿到用户手机的shell权限。接下来攻击者就可以在后台默默安装木马,完全洞穿用户的手机。详细的攻击过程可以见乌云平台的 这份报告:WebView中接口隐患与手机挂马利用。

安卓4.2及以上版本(API >= 17),在注入类中为可调用的方法添加@JavascriptInterface注解,无注解的方法不能被调用,这种方式可以防范注入漏洞。那么有没一种 安全的方式,可以完全兼顾安卓4.2以下版本呢?答案就是使用prompt,即WebChromeClient 输入框弹出模式。

我们参照 Android WebView的Js对象注入漏洞解决方案 这篇文章给出的解决方案, 但它JS下的方法有点笨拙, 动态生成JS文件过程也并没有清晰,且加载JS文件的时机也没有准确把握。那么如何改造才能便利地在JS代码中调用Java方法,并且安全可靠呢?

一、动态地生成将注入的JS代码

JsCallJava在构造时,将要注入类的public且static方法拿出来,逐个生成方法的签名,依据方法签名先将方法缓存起来,同时结合方法名称与静态的HostApp-JS代码动态生成一段将要注入到webview中的字符串。

publicJsCallJava (String injectedName, Class injectedCls) {

try{

mMethodsMap = newHashMap();

//获取自身声明的所有方法(包括public private protected), getMethods会获得所有继承与非继承的方法

Method[] methods = injectedCls.getDeclaredMethods();

StringBuilder sb = newStringBuilder("javascript:(function(b){console.log(\"HostApp initialization begin\");var a={queue:[],callback:function(){var d=Array.prototype.slice.call(arguments,0);var c=d.shift();var e=d.shift();this.queue[c].apply(this,d);if(!e){delete this.queue[c]}}};");

for(Method method : methods) {

String sign;

if(method.getModifiers() != (Modifier.PUBLIC | Modifier.STATIC) || (sign = genJavaMethodSign(method)) ==null) {

continue;

}

mMethodsMap.put(sign, method);

sb.append(String.format("a.%s=", method.getName()));

}

sb.append("function(){var f=Array.prototype.slice.call(arguments,0);if(f.length<1){throw\"HostApp call error, message:miss method name\"}var e=[];for(var h=1;h

mPreloadInterfaceJS = sb.toString();

} catch(Exception e){

Log.e(TAG, "init js error:"+ e.getMessage());

}

}

privateString genJavaMethodSign (Method method) {

String sign = method.getName();

Class[] argsTypes = method.getParameterTypes();

intlen = argsTypes.length;

if(len <1|| argsTypes[0] != WebView.class) {

Log.w(TAG, "method("+ sign +") must use webview to be first parameter, will be pass");

returnnull;

}

for(intk =1; k

Class cls = argsTypes[k];

if(cls == String.class) {

sign += "_S";

} elseif(cls ==int.class||

cls == long.class||

cls == float.class||

cls == double.class) {

sign += "_N";

} elseif(cls ==boolean.class) {

sign += "_B";

} elseif(cls == JSONObject.class) {

sign += "_O";

} elseif(cls == JsCallback.class) {

sign += "_F";

} else{

sign += "_P";

}

}

returnsign;

}

从上面可以看出,类的各个方法名称被拼接到前后两段静态压缩的JS代码当中,那么这样生成的完整清晰的HostApp-JS片段是怎样的呢? 我们假设HostJsScope类中目前只定义了toast、alert、getIMSI这三个公开静态方法,那么完整的片段就是下面这样:

(function(global){

console.log("HostApp initialization begin");

var hostApp = {

queue: [],

callback: function () {

var args = Array.prototype.slice.call(arguments, 0);

var index = args.shift();

var isPermanent = args.shift();

this.queue[index].apply(this, args);

if(!isPermanent) {

delete this.queue[index];

}

}

};

hostApp.toast = hostApp.alert = hostApp.getIMSI = function () {

var args = Array.prototype.slice.call(arguments, 0);

if(args.length <1) {

throw"HostApp call error, message:miss method name";

}

var aTypes = [];

for(var i =1;i

var arg = args[i];

var type = typeof arg;

aTypes[aTypes.length] = type;

if(type =="function") {

var index = hostApp.queue.length;

hostApp.queue[index] = arg;

args[i] = index;

}

}

var res = JSON.parse(prompt(JSON.stringify({

method: args.shift(),

types: aTypes,

args: args

})));

if(res.code !=200) {

throw"HostApp call error, code:"+ res.code +", message:"+ res.result;

}

returnres.result;

};

//有时候,我们希望在该方法执行前插入一些其他的行为用来检查当前状态或是监测

//代码行为,这就要用到拦截(Interception)或者叫注入(Injection)技术了

/**

* Object.getOwnPropertyName 返回一个数组,内容是指定对象的所有属性

*

* 其后遍历这个数组,分别做以下处理:

* 1. 备份原始属性;

* 2. 检查属性是否为 function(即方法);

* 3. 若是重新定义该方法,做你需要做的事情,之后 apply 原来的方法体。

*/

Object.getOwnPropertyNames(hostApp).forEach(function (property) {

var original = hostApp[property];

if(typeof original ==='function'&&property!=="callback") {

hostApp[property] = function () {

returnoriginal.apply(hostApp,  [property].concat(Array.prototype.slice.call(arguments,0)));

};

}

});

global.HostApp = hostApp;

console.log("HostApp initialization end");

})(window);

其实在JsCallJava初始化时我们拼接的只是上面第15行 hostApp.toast = hostApp.alert = hostApp.getIMSI = function () 这段。目的是将所有JS层调用函数嫁接到一个匿名函数1中,而后利用拦截技术,遍历hostApp下所有的函数,拿出对应的函数名,然后将hostApp 下所有的函数调用嫁接到另一个匿名函数2,这样做的目的是hostApp下函数调用时首先执行匿名函数2,匿名函数2将对应的函数名作为第一个参数然后再 调用匿名函数1,这样匿名函数1中就能区分执行时调用来源。实现了JS层调用入口统一,返回出口统一的结构体系。

二、HostApp JS片段注入时机

步骤一说明了HostApp-JS片段的拼接方法,同时JS片段拼接是在JsCallJava初始化完成的,而JsCallJava初始化是在实例化InjectedChromeClient对象时发起的。

publicInjectedChromeClient (String injectedName, Class injectedCls) {

mJsCallJava = newJsCallJava(injectedName, injectedCls);

}

从步骤一的代码,我们知道JsCallJava拼接出来的JS代码暂时被存到mPreloadInterfaceJS字段中。那么我们何时把这段代码串注入到Webview的页面空间内呢?答案是页面加载进度变化的过程中。

@Override

publicvoidonProgressChanged (WebView view,intnewProgress) {

//为什么要在这里注入JS

//1 OnPageStarted中注入有可能全局注入不成功,导致页面脚本上所有接口任何时候都不可用

//2 OnPageFinished中注入,虽然最后都会全局注入成功,但是完成时间有可能太晚,当页面在初始化调用接口函数时会等待时间过长

//3 在进度变化时注入,刚好可以在上面两个问题中得到一个折中处理

//为什么是进度大于25%才进行注入,因为从测试看来只有进度大于这个数字页面才真正得到框架刷新加载,保证100%注入成功

if(newProgress <=25) {

mIsInjectedJS = false;

} elseif(!mIsInjectedJS) {

view.loadUrl(mJsCallJava.getPreloadInterfaceJS());

mIsInjectedJS = true;

Log.d(TAG, " inject js interface completely on progress "+ newProgress);

}

super.onProgressChanged(view, newProgress);

}

从上面我们可以看出,注入的时机是准确把握在进度大于25%时。如果在OnPageFinished注入,页面document.ready的初始回调会等待时间过长,详细的原因我们会在后面讲到。

三、页面调用Java方法执行的过程

OK,上面两步解决了动态生成与成功注入的两大问题,接下来就要处理JS具体的调用过程。上面,我们知道页面调用Java方法时,匿名js函数在拼 接好参数后prompt json数据。prompt消息被Java层的WebChromeClient.onJsPrompt拦截到。

@Override

publicbooleanonJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {

result.confirm(mJsCallJava.call(view, message));

returntrue;

}

而JsCallJava.call的具体实现如下。

publicString call(WebView webView, String jsonStr) {

if(!TextUtils.isEmpty(jsonStr)) {

try{

JSONObject callJson = newJSONObject(jsonStr);

String methodName = callJson.getString("method");

JSONArray argsTypes = callJson.getJSONArray("types");

JSONArray argsVals = callJson.getJSONArray("args");

String sign = methodName;

intlen = argsTypes.length();

Object[] values = newObject[len +1];

intnumIndex =0;

String currType;

values[0] = webView;

for(intk =0; k

currType = argsTypes.optString(k);

if("string".equals(currType)) {

sign += "_S";

values[k + 1] = argsVals.isNull(k) ?null: argsVals.getString(k);

} elseif("number".equals(currType)) {

sign += "_N";

numIndex = numIndex * 10+ k +1;

} elseif("boolean".equals(currType)) {

sign += "_B";

values[k + 1] = argsVals.getBoolean(k);

} elseif("object".equals(currType)) {

sign += "_O";

values[k + 1] = argsVals.isNull(k) ?null: argsVals.getJSONObject(k);

} elseif("function".equals(currType)) {

sign += "_F";

values[k + 1] =newJsCallback(webView, argsVals.getInt(k));

} else{

sign += "_P";

}

}

Method currMethod = mMethodsMap.get(sign);

// 方法匹配失败

if(currMethod ==null) {

returngetReturn(jsonStr,500,"not found method("+ methodName +") with valid parameters");

}

// 数字类型细分匹配

if(numIndex >0) {

Class[] methodTypes = currMethod.getParameterTypes();

intcurrIndex;

Class currCls;

while(numIndex >0) {

currIndex = numIndex - numIndex / 10*10;

currCls = methodTypes[currIndex];

if(currCls ==int.class) {

values[currIndex] = argsVals.getInt(currIndex - 1);

} elseif(currCls ==long.class) {

//WARN: argsJson.getLong(k + defValue) will return a bigger incorrect number

values[currIndex] = Long.parseLong(argsVals.getString(currIndex - 1));

} else{

values[currIndex] = argsVals.getDouble(currIndex - 1);

}

numIndex /= 10;

}

}

returngetReturn(jsonStr,200, currMethod.invoke(null, values));

} catch(Exception e) {

//优先返回详细的错误信息

if(e.getCause() !=null) {

returngetReturn(jsonStr,500,"method execute error:"+ e.getCause().getMessage());

}

returngetReturn(jsonStr,500,"method execute error:"+ e.getMessage());

}

} else{

returngetReturn(jsonStr,500,"call data empty");

}

}

这是一个完整的解析匹配过程,会依据js层传入的方法名、参数类型列表再次生成方法签名,与之前初始化构造好的缓存对象中的方法匹配。匹配成功后则 判断js调用参数类型中是否有number类型,如果有依据Java层方法的定义决定是取int、long还是double类型的值。最后使用调用值列表 和方法对象反射执行,返回函数执行的结果。这里有几点需要注意:

方法反射执行时会将当前WebView的实例放到第一个参数,方便在HostJsScope静态方法依据Context拿到一些相关上下文信息;

注入类(如HostJsScope)静态方法的参数定义可使用的类型有int/long/double、String、boolean、 JSONObject、JsCallback,对应于js层传入的类型为number、string、boolean、object、function, 注意number数字过大时(如时间戳),可能需要先转为string类型(Java方法中参数也须定义为String),避免精度丢失;

Java方法的返回值可以是void 或 能转为字符串的类型(如int、long、String、double、float等)或 可序列化的自定义类型;

如果执行失败或找不到调用方法时,Java层会将异常信息传递到JS层, JS匿名函数中会throw抛出错误;

四、HostApp在页面的使用

有了上面的准备工作,现在我们在页面中就可以很方便地使用HostApp了,而不需要加载任何依赖文件。如li标签的点击:

HostApp.alert

HostApp.toast

HostApp.testLossTime

HostApp.getIMSI

但同时有一种业务情景时,页面初始加载完备时就应立即触发的调用,如果我们这样写:

document.addEventListener('DOMContentLoaded', function() {

HostApp.toast('document ready now');;

}, false);

那么HostApp的调用极有可能不成功,因为端注入HostApp-JS片段的时机可能在document.ready前也可能在其后。那么如何解决这个矛盾的问题呢?

如果document.ready的时候HostApp JS已经注入成功,这种情况OK没有问题。当document.ready的时候HostApp JS还未开始注入,这种情景下我们的js脚本层就需要做出变动,即轮询状态,直到端注入成功或者超时(1.5s),再发生回调。具体实现如下(下面的是以 zepto.js的$.ready()函数改造为例)。

//针对DOM的一些操作

// Define methods that will be available on all

// Zepto collections

$.fn = {

//DOM Ready

ready: function(callback, jumpHostAppInject) {

var originCb = callback;

var mcounter = 0;

//尝试等待(1500ms超时)让端注入HostApp Js

callback = function () {

if(!window.HostApp && mcounter++ <150)setTimeout(callback,10);elseoriginCb($);

};

//是否跳过等待HostApp的注入

if(jumpHostAppInject) {

callback = originCb;

}

if(readyRE.test(document.readyState)) callback($);elsedocument.addEventListener('DOMContentLoaded', function() {

callback($)

}, false);

returnthis

},

...

...

};

这样的机制也就解释了为什么不把Java层的JS注入放在OnPageFinish了,如果那样页面轮询的次数就会上升,等待的时间就会变长,而且有可能会超时。好了,有了上面的改动,页面初始加载完备时需要立即触发HostApp的调用,如下:

$(function () {

HostApp.alert("HostApp ready now");

});

【编辑推荐】

【责任编辑:闫佳明 TEL:(010)68476606】

点赞 0

java 防止js注入_在WebView中如何让JS与Java安全地互相调用相关推荐

  1. java 传递字符串数组_将数组中字符串的值传递给java中的方法?

    参见英文答案 > How do I compare strings in Java?                                    23个 感谢您抽出时间来阅读.对不起, ...

  2. java.lang.IllegalArgumentException: 字符[_]在域名中永远无效。 at

    [http-nio-8080-exec-1] org.apache.coyote.AbstractProcessor.parseHost [xxx_tomcat] 是无效主机注意:更多的请求解析错误将 ...

  3. webview 防止js注入_天台县js聚合物水泥防水涂料的作用

    天台县js聚合物水泥防水涂料的作用 2.JS防水涂料建筑胶粉与801建筑胶水的区别在哪?彩色聚氨酯防水涂料施工要点: 彩色聚氨酯防水涂料注意事项: 1.材料必须按配比混合搅拌均匀,混合后的物料应在20 ...

  4. python 加载动图_在浏览器中使用TensorFlow.js和Python构建机器学习模型(附代码)...

    大数据文摘授权转载自数据派THU 作者:MOHD SANAD ZAKI RIZVI 本文主要介绍了: TensorFlow.js (deeplearn.js)使我们能够在浏览器中构建机器学习和深度学习 ...

  5. java 字符串 字节数组_字符串到字节数组,字节数组到Java中的字符串

    java 字符串 字节数组 Today we will learn how to convert String to byte array in java. We will also learn ho ...

  6. java 数字 下划线_数字文字中的下划线– Java 7功能

    java 数字 下划线 One of the Java 7 features is underscores in numeric literals. You can place underscores ...

  7. java加载顺序_类加载过程中几个重点执行顺序整理

    正文前先来一波福利推荐: 福利一: 百万年薪架构师视频,该视频可以学到很多东西,是本人花钱买的VIP课程,学习消化了一年,为了支持一下女朋友公众号也方便大家学习,共享给大家. 福利二: 毕业答辩以及工 ...

  8. java instanceof性能差_在J中使用instanceof的性能影响

    在J中使用instanceof的性能影响 我正在开发一个应用程序,一种设计方法涉及到instanceof运算符的极大使用. 虽然我知道OO设计通常会试图避免使用instanceof,但这是一个不同的故 ...

  9. java处理json特殊字符_如何处理JSON中的特殊字符

    JSON 是适用于 Ajax 应用程序的一种有效格式,原因是它使 JavaScript 对象和字符串值之间得以快速转换.由于 Ajax 应用程序非常适合将纯文本发送给服务器端程序并对应地接收纯文本,相 ...

最新文章

  1. 架构选型必读:集中式与分布式全方位优劣对比
  2. MySQL 唯一索引和普通索引该如何选择?
  3. spring 依赖注入总结
  4. flutter图片预览_Flutter 视频缩略图
  5. linux中查看进程
  6. LeetCode篇之栈:155(常数时间复杂度内找最小栈)
  7. linux dac 的权限,Samba CAP_DAC_OVERRIDE文件权限绕过安全限制漏洞
  8. Silverlight4 ColorPicker控件
  9. Java基于opencv实现图像数字识别(一),java开发面试笔试题
  10. python wmi 显卡型号_python - wmi模块学习(windwos硬件信息获取)
  11. gulp html 缓存,gulp 给静态资源文件添加hash(md5)后缀 防止缓存
  12. C语言入栈算法,栈的入栈、出栈、获取栈顶的c语言算法
  13. 银联无卡涉及关键词整理
  14. cad记忆口诀_CAD的制图口诀
  15. 查看ftp服务器里的文件,查看ftp服务器所有文件
  16. Mac Mounty挂载NTFS硬盘报错
  17. oracle 存储过程好学吗,想靠Oracle拿高薪,存储过程的优点你是否搞清楚了?
  18. Objective-C中的消息发送总结
  19. Android插件化原理和实践 (一) 之 插件化简介和基本原理简述
  20. 安卓机——旧机器焕发新活力

热门文章

  1. 真实赛车3,SPEEDRUSH TV 第3季,第3阶段(第3天),直线加速赛
  2. 基于AJAX技术的Web会议平台
  3. 深度学习中Attention机制的“前世今生”
  4. 仅用语音生成人体姿态,代码已开源
  5. 近期机器学习竞赛汇总~总奖池超三百万人民币!
  6. IoU-aware的目标检测,显著提高定位精度
  7. 文远知行2020年校招 | 领航者计划
  8. Python for 循环语句-Python 基础教程
  9. 月薪3K与月薪3万的程序员,距离是怎么拉开的?
  10. python 基础 - 循环语句