Android中Java和JS的交互

随着H5性能的提升,在我们移动应用开发的过程中,我们会越来越多的在我们的App页面内嵌入H5页面,使得App变的更加动态灵活。而H5页面往往并不是独立,很多时候需要和native进行交互,调用native的一些方法,或者Web中的一些方法被native所调用。

现在有很多开源的解决方案,比如JSBridge,可以很方便的让我们进行web页面和native的交互,其实现是在WebView原有提供的Web和Native通信基础上做了封装,由于最近接手工作中用到了JSBridge,借此机会学习了一下,本文先从系统提供的一些WebView和Native交互接口讲起,然后对于一个简单的开源JSBridge 的剖析。

WebView的使用

 WebView  webView= (WebView) findViewById(R.id.webview);
 webView.loadUrl("file:///android_asset/index.html");

我们可以通过将WebView内嵌在App界面中,来装载网页,通过loadUrl,给予一个本地或者远程的地址,程序执行即可装载出我们的界面。这里代码演示的是在assets文件下一个index.html文件,然后通过我们的Webview装载的。

Android中 JS和Java的交互方式

在进行交互之前需要我们对WebView进行设置开启对JS的支持。

WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
  • Java调用JS
    • 通过WebView的loadUrl()
    • 通过WebView的evaluateJavascript()
  • JS调用Java
    • 通过WebView的JavascriptInterface
    • 通过WebViewClient.shouldOverrideUrlLoading(),拦截加载信息
    • 通过WebChromeClient.onConsoleMessage(),拦截控制台信息
    • 通过WebChromeClient.onJsPrompt(),onJsAlert()、onJsConfirm()拦截Web相应弹框的事件

Java调用JS

在Java中调用JS的代码有两种方式,分别为通过loadurl和通过evaluateJavascript.

首先定义了一个html文件,然后将其放置在asset目录下。通过WebView loadUrl装载。

<html>

<head>
    <title>我的页面</title>
    <meta charset=utf-8> 
    <script type="application/javascript">
        function alertTest() {
            alert("alerttest");
        }
    </script>
</head>

<body>
    <p>Android Java JS 交互测试</p>
</body>

</html>
  • 通过loadUrl来调用JS方法
mWebView.post(new Runnable() {
            @Override
            public void run() {
                mWebView.loadUrl("javascript:alertTest()");
            }
        });
  • 通过evaluateJavascript来调用JS方法

通过该方法,我们还可以得到JS方法的返回值,来进行值的展示。

mWebView.evaluateJavascript("javascript:alertTest()", new ValueCallback<String>() {
            @Override
            public void onReceiveValue(String value) {
                Toast.makeText(WebViewActivity.this, value, Toast.LENGTH_SHORT).show();
            }
        });

这两个方法在开始调用的时候,出现的问题是报出错误信息,错误信息表示调用的JS方法未被定义,问题原因是因为在oncreate或者onResume方法中调用的时候,其JavaScript文件未被完全加载完成,因此出现了该问题,可以通过监听WebView的装载事件延迟调用来解决该问题。

JS调用Java

  • JavascriptInterface

该种方式由于存在着缺陷,后来被弃用。具体问题将在下面介绍。这里先讲一下其使用的方式。

1.定义和JS相关的交互类和方法,对于方法通过注解进行标注。

public class JSTest {

  private Context mContext;

  public JSTest(Context context) {
      mContext = context;
   }

  @JavascriptInterface
  public void showToast(String str) {
       Toast.makeText(mContext, str, Toast.LENGTH_SHORT).show();
  }
}

2.向WebView添加该JavaScriptInterface,同时为其指定一个名称,该名称将会在JS文件中使用。

 mWebView.addJavascriptInterface(new JsTest(context), "JsTest");
  1. JS文件
 function showToast() {
        JsTest.showToast("来自Web调用");
 }
测试
  • shouldOverrideUrlLoading

在WebViewClient中有一个方法shouldOverrideUrlLoading,该方法在每次有新的链接跳转的时候,该函数都会被回调,同时传递该次跳转的url,所以我们可以根据自己的需求制定一个url的规则,在这里对于url进行判断,如果是我们协议内的,则进行拦截,解析我们的协议,然后进行相应的方法调用。

  • onConsoleMessage()
    在WebChromeClient中,有一个函数回调,当我们有console消息的时候,该函数就会被回调到。因此,我们可以自己制定规则,然后触发console消息,这个时候该函数就会被回调,回调之后,根据我们的规则进行解析,然后调用我们本地相应的方法。

1.在WebChromeClient中定义相应回调方法的拦截处理

 @Override
    public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
        String msg = consoleMessage.message();
        if ("showToast".equals(msg)) {
            Toast.makeText(mContext, "来自Web Toast测试", Toast.LENGTH_SHORT).show();
        }
        return super.onConsoleMessage(consoleMessage);
    }
  1. JS文件,向控制台输出信息。
 function consoleTest() {
     console.log("showToast");
 }
demo展示
  • onJsPrompt,onJsConfirm, onJsAlert

除了onConsoleMessage的回调之外,WebChromeClient还提供了onJsConfirm,onJsAlert,onJSPrompt等回调,这些在web端有相应的操作的时候,都会被回调到。对于其拦截,要对其中的result做判断和处理,返回值为true,则表示不再执行,如果返回值不是true,则会网页上的操作继续被执行。我们可以通过该种消息的回调来传递一些信息,通过这个信息来实现JS和Java的交互。

对于三种回调的方式,onJsPrompt可以传递一个任意的值给web,而JsConfirm只能传递是否,onJsAlert则不能够传递值,因此为了实现JS和Java的互相调用,onJsPrompt使用是最方便的。

1.JS文件

function promptTest {
       var result = prompt("js://test?arg1=device");
      alert("device: " + result);
}

2.WebChromeClient中做相应的拦截,这里直接按照传递的数据做比对处理,没有做协议的约束和解析,然后返回一个当前的设备类型。

 @Override
 public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
     if ("js://test?arg1=device".equals(message)) {
        result.confirm("模拟器");
     }
      return true;
}
Demo演示

JSBridge的实现

上面分析了JS和Java的交互的方式,但是如果只是通过上述的方式进行通信,必然会使得代码比较臃肿,也难以维护,因此就出现了各种框架来对其进行封装。这里给出一个简单地通信包装。

我们的需求是实现JS和Java的互相调用,比如JS调用了Java的方法,执行完成之后,能够将结果返回,同时使用返回的结果作为JS方法的参数,执行相应的JS方法。这里采取的通信方式是通过对onJsPrompt的拦截解析,然后通过loadUrl的方式执行JS方法。这里分析的是一个简单开源JSBridge的实现。

JSBridge实现
  • Java方法处理

对于JS可能会调用到的Java方法,进行集中管理,对于每一个类,可以自定义名称,方便在JS中的调用。可能会被调用到的Java方法,都要进行注册。

public class JSBridge {

    private static Map<String, HashMap<String, Method>> exposedMethods = new HashMap<>();

    public static void register(String exposedName, Class<? extends IBridge> clazz) {
        if (!exposedMethods.containsKey(exposedName)) {
            try {
                exposedMethods.put(exposedName, getAllMethod(clazz));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
      ....
}

除此之外,还提供了一个调用函数,这个函数主要是对传递的数据根据我们制定的协议进行解析,然后从注册的函数中找到所要调用的函数,执行Java函数。

public static String invokeNative(WebView webView, String uriString) {
      //协议解析
        String methodName = "";
        String className = "";
        String param = "{}";
        String port = "";
        if (!TextUtils.isEmpty(uriString) && uriString.startsWith("JSBridge")) {
            Uri uri = Uri.parse(uriString);
            className = uri.getHost();
            param = uri.getQuery();
            port = uri.getPort() + "";
            String path = uri.getPath();
            if (!TextUtils.isEmpty(path)) {
                methodName = path.replace("/", "");
            }
        }

      //查找方法,执行相应函数
        if (exposedMethods.containsKey(className)) {
            HashMap<String, Method> methodHashMap = exposedMethods.get(className);

            if (methodHashMap != null && methodHashMap.size() != 0 && methodHashMap.containsKey(methodName)) {
                Method method = methodHashMap.get(methodName);
                if (method != null) {
                    try {
                        method.invoke(null, webView, new JSONObject(param), new Callback(webView, port));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return null;
    }

从上面函数执行的语句中,可以看到起传递的参数有WebView,JSonObject和一个Callback。

method.invoke(null, webView, new JSONObject(param), new Callback(webView, port));

这里用来给JS调用的Java方法,传递的值都是JsonObject的形式,Callback则是回调相应的JS方法,当我们的Java方法执行完成之后,如果我们需要调用相应的JS方法,我们可以通过callback提供的apply方法来传递一些数据。

    public static void showToast(WebView webView, JSONObject param, final Callback callback) {
        String message = param.optString("msg");
        Toast.makeText(webView.getContext(), message, Toast.LENGTH_SHORT).show();
        if (null != callback) {
            try {
                JSONObject object = new JSONObject();
                object.put("key", "value");
                object.put("key1", "value1");
                callback.apply(getJSONObject(0, "ok", object));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

这里callback的apply方法的实现。

private static final String CALLBACK_JS_FORMAT = "javascript:JSBridge.onFinish('%s', %s);";

public void apply(JSONObject jsonObject) {
     final String execJs = String.format(CALLBACK_JS_FORMAT, mPort, String.valueOf(jsonObject));
     if (mWebViewRef != null && mWebViewRef.get() != null) {
         mHandler.post(new Runnable() {
           @Override
            public void run() {
               mWebViewRef.get().loadUrl(execJs);
           }
         });
     }
  }

这里将需要传回的数据,传递给JSBridge中的onFinish方法。传递的数据中有一个端口号,通过这个端口号作为标示,来调用相应的方法。

这里方法注册的方式会局限在某一些类中,而且需要我们提前对所有的方法进行注册,同时对于实例方法和静态方法,在执行上也会有一些区分,对于方法的处理,可以通过两端的协商,规定好调用的方法之后,可以设置一个处理类,然后对于每一个持有WebView的Activity,为其设置一个回调,当JS对传输的参数进行解析的时候,解析返回的数据,进行相应的处理即可。这样就可以调用到我们当前的类,但是对于方法调用部分就需要我们手动去处理,而非在JSBridge中作为一个黑盒处理。

  • JS方法处理
callbacks: {},
call: function (obj, method, params, callback) {
      var port = Util.getPort();
      this.callbacks[port] = callback;
      var uri=Util.getUri(obj,method,params,port);
      window.prompt(uri, "");
},
onFinish: function (port, jsonObj){
     var callback = this.callbacks[port];
     callback && callback(jsonObj);
     delete this.callbacks[port];
},

JS文件提供了两个方法,一个是call一个是finish,分别是web中被调用,另一个是在native中将会被调用,每一个web中调用我们native方法的时候,都会调用js文件中的oncall方法,同时也会传递一个函数作为回调,js文件中会为该次调用随机生成一个端口号,同时将其回调保存在一个内部列表callbacks中,根据协议规则,通过window.promt的方式将相应的调用传递下去。

public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
    result.confirm(JSBridge.invokeNative(view, message));
    return true;
}

在WebChromeClient的onJsPrompt方法中便可以得到相应的信息,通过JSBridge方法对回传数据进行相应的调用。

JS中调用Java方法的方式

JSBridge.call('bridge','showToast',{'msg':'Hello JSBridge'},function(res){alert(JSON.stringify(res))})

第一,二个参数表示调用的Java的方法,第三个参数为传递的参数。第四个参数为设置的回调函数。

至此,一个简单的JSBridge实现了,JS只需要通过call方法传递相应的参数即可调用Java方法,Java只需要将待调用方法进行注册即可。

交互中的安全漏洞问题

进几年和WebView远程代码执行相关的漏洞主要有CVE-2012-6336,CVE-2014-1939,CVE-2014-7224, 这些漏洞中最核心的漏洞是CVE-2012-6336,另外两个CVE只是发现了几个默认存在的接口。

  • CVE-2012-6636

Android API 16.0及之前的版本中存在安全漏洞,该漏洞源于程序没有正确限制使用WebView.addJavascriptInterface方法。远程攻击者可通过使用Java Reflection API利用该漏洞执行任意Java对象的方法

Google Android <= 4.1.2 (API level 16) 受到此漏洞的影响。

  • CVE-2014-1939

java/android/webkit/BrowserFrame.java 使用addJavascriptInterface API并创建了SearchBoxImpl类的对象。攻击者可通过访问searchBoxJavaBridge_接口利用该漏洞执行任意Java代码。

Google Android <= 4.3.1 受到此漏洞的影响

  • CVE-2014-7224

香港理工大学的研究人员发现当系统辅助功能中的任意一项服务被开启后,所有由系统提供的WebView都会被加入两个JS objects,分别为是accessibility和accessibilityTraversal。恶意攻击者就可以使用accessibility和accessibilityTraversal这两个Java Bridge来执行远程攻击代码.

Google Android < 4.4 受到此漏洞的影响。

对于上述漏洞,其攻击原理为得到了Java对象,通过反射的方式来执行自己的恶意代码。

解决方案

1.移除掉原有提供的JavaScript接口

private static final void removeJavascriptInterfaces11(WebView webView) {
     try {
       webView.removeJavascriptInterface("searchBoxJavaBridge_");

       webView.removeJavascriptInterface("accessibility");          

        webView.removeJavascriptInterface("accessibilityTraversal");
      } catch (Throwable tr) {
            tr.printStackTrace();
     }
}   

2.升级系统API level 17后,只有显示添加 @JavascriptInterface的方法才能被JavaScript调用,这样反射就失去作用了。但对于更低版本则还是会存在,考虑采用其它方案,例如JSBridge实现交互。

参考资料

JsBridge 实现 JavaScript 和 Java 的互相调用

Android:你要的WebView与 JS 交互方式 都在这里

Android WebView远程执行代码漏洞浅析

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容