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远程执行代码漏洞浅析

推荐阅读更多精彩内容