Android中Java和JS的交互

隨著H5性能的提升,在我們移動(dòng)應(yīng)用開發(fā)的過程中,我們會(huì)越來越多的在我們的App頁面內(nèi)嵌入H5頁面,使得App變的更加動(dòng)態(tài)靈活。而H5頁面往往并不是獨(dú)立,很多時(shí)候需要和native進(jìn)行交互,調(diào)用native的一些方法,或者Web中的一些方法被native所調(diào)用。

現(xiàn)在有很多開源的解決方案,比如JSBridge,可以很方便的讓我們進(jìn)行web頁面和native的交互,其實(shí)現(xiàn)是在WebView原有提供的Web和Native通信基礎(chǔ)上做了封裝,由于最近接手工作中用到了JSBridge,借此機(jī)會(huì)學(xué)習(xí)了一下,本文先從系統(tǒng)提供的一些WebView和Native交互接口講起,然后對(duì)于一個(gè)簡單的開源JSBridge 的剖析。

WebView的使用

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

我們可以通過將WebView內(nèi)嵌在App界面中,來裝載網(wǎng)頁,通過loadUrl,給予一個(gè)本地或者遠(yuǎn)程的地址,程序執(zhí)行即可裝載出我們的界面。這里代碼演示的是在assets文件下一個(gè)index.html文件,然后通過我們的Webview裝載的。

Android中 JS和Java的交互方式

在進(jìn)行交互之前需要我們對(duì)WebView進(jìn)行設(shè)置開啟對(duì)JS的支持。

WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
  • Java調(diào)用JS
    • 通過WebView的loadUrl()
    • 通過WebView的evaluateJavascript()
  • JS調(diào)用Java
    • 通過WebView的JavascriptInterface
    • 通過WebViewClient.shouldOverrideUrlLoading(),攔截加載信息
    • 通過WebChromeClient.onConsoleMessage(),攔截控制臺(tái)信息
    • 通過WebChromeClient.onJsPrompt(),onJsAlert()、onJsConfirm()攔截Web相應(yīng)彈框的事件

Java調(diào)用JS

在Java中調(diào)用JS的代碼有兩種方式,分別為通過loadurl和通過evaluateJavascript.

首先定義了一個(gè)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來調(diào)用JS方法
mWebView.post(new Runnable() {
            @Override
            public void run() {
                mWebView.loadUrl("javascript:alertTest()");
            }
        });
  • 通過evaluateJavascript來調(diào)用JS方法

通過該方法,我們還可以得到JS方法的返回值,來進(jìn)行值的展示。

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

這兩個(gè)方法在開始調(diào)用的時(shí)候,出現(xiàn)的問題是報(bào)出錯(cuò)誤信息,錯(cuò)誤信息表示調(diào)用的JS方法未被定義,問題原因是因?yàn)樵趏ncreate或者onResume方法中調(diào)用的時(shí)候,其JavaScript文件未被完全加載完成,因此出現(xiàn)了該問題,可以通過監(jiān)聽WebView的裝載事件延遲調(diào)用來解決該問題。

JS調(diào)用Java

  • JavascriptInterface

該種方式由于存在著缺陷,后來被棄用。具體問題將在下面介紹。這里先講一下其使用的方式。

1.定義和JS相關(guān)的交互類和方法,對(duì)于方法通過注解進(jìn)行標(biāo)注。

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,同時(shí)為其指定一個(gè)名稱,該名稱將會(huì)在JS文件中使用。

 mWebView.addJavascriptInterface(new JsTest(context), "JsTest");
  1. JS文件
 function showToast() {
        JsTest.showToast("來自Web調(diào)用");
 }
測試
  • shouldOverrideUrlLoading

在WebViewClient中有一個(gè)方法shouldOverrideUrlLoading,該方法在每次有新的鏈接跳轉(zhuǎn)的時(shí)候,該函數(shù)都會(huì)被回調(diào),同時(shí)傳遞該次跳轉(zhuǎn)的url,所以我們可以根據(jù)自己的需求制定一個(gè)url的規(guī)則,在這里對(duì)于url進(jìn)行判斷,如果是我們協(xié)議內(nèi)的,則進(jìn)行攔截,解析我們的協(xié)議,然后進(jìn)行相應(yīng)的方法調(diào)用。

  • onConsoleMessage()
    在WebChromeClient中,有一個(gè)函數(shù)回調(diào),當(dāng)我們有console消息的時(shí)候,該函數(shù)就會(huì)被回調(diào)到。因此,我們可以自己制定規(guī)則,然后觸發(fā)console消息,這個(gè)時(shí)候該函數(shù)就會(huì)被回調(diào),回調(diào)之后,根據(jù)我們的規(guī)則進(jìn)行解析,然后調(diào)用我們本地相應(yīng)的方法。

1.在WebChromeClient中定義相應(yīng)回調(diào)方法的攔截處理

 @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文件,向控制臺(tái)輸出信息。
 function consoleTest() {
     console.log("showToast");
 }
demo展示
  • onJsPrompt,onJsConfirm, onJsAlert

除了onConsoleMessage的回調(diào)之外,WebChromeClient還提供了onJsConfirm,onJsAlert,onJSPrompt等回調(diào),這些在web端有相應(yīng)的操作的時(shí)候,都會(huì)被回調(diào)到。對(duì)于其攔截,要對(duì)其中的result做判斷和處理,返回值為true,則表示不再執(zhí)行,如果返回值不是true,則會(huì)網(wǎng)頁上的操作繼續(xù)被執(zhí)行。我們可以通過該種消息的回調(diào)來傳遞一些信息,通過這個(gè)信息來實(shí)現(xiàn)JS和Java的交互。

對(duì)于三種回調(diào)的方式,onJsPrompt可以傳遞一個(gè)任意的值給web,而JsConfirm只能傳遞是否,onJsAlert則不能夠傳遞值,因此為了實(shí)現(xiàn)JS和Java的互相調(diào)用,onJsPrompt使用是最方便的。

1.JS文件

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

2.WebChromeClient中做相應(yīng)的攔截,這里直接按照傳遞的數(shù)據(jù)做比對(duì)處理,沒有做協(xié)議的約束和解析,然后返回一個(gè)當(dāng)前的設(shè)備類型。

 @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的實(shí)現(xiàn)

上面分析了JS和Java的交互的方式,但是如果只是通過上述的方式進(jìn)行通信,必然會(huì)使得代碼比較臃腫,也難以維護(hù),因此就出現(xiàn)了各種框架來對(duì)其進(jìn)行封裝。這里給出一個(gè)簡單地通信包裝。

我們的需求是實(shí)現(xiàn)JS和Java的互相調(diào)用,比如JS調(diào)用了Java的方法,執(zhí)行完成之后,能夠?qū)⒔Y(jié)果返回,同時(shí)使用返回的結(jié)果作為JS方法的參數(shù),執(zhí)行相應(yīng)的JS方法。這里采取的通信方式是通過對(duì)onJsPrompt的攔截解析,然后通過loadUrl的方式執(zhí)行JS方法。這里分析的是一個(gè)簡單開源JSBridge的實(shí)現(xiàn)。

JSBridge實(shí)現(xiàn)
  • Java方法處理

對(duì)于JS可能會(huì)調(diào)用到的Java方法,進(jìn)行集中管理,對(duì)于每一個(gè)類,可以自定義名稱,方便在JS中的調(diào)用。可能會(huì)被調(diào)用到的Java方法,都要進(jìn)行注冊(cè)。

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();
            }
        }
    }
      ....
}

除此之外,還提供了一個(gè)調(diào)用函數(shù),這個(gè)函數(shù)主要是對(duì)傳遞的數(shù)據(jù)根據(jù)我們制定的協(xié)議進(jìn)行解析,然后從注冊(cè)的函數(shù)中找到所要調(diào)用的函數(shù),執(zhí)行Java函數(shù)。

public static String invokeNative(WebView webView, String uriString) {
      //協(xié)議解析
        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("/", "");
            }
        }

      //查找方法,執(zhí)行相應(yīng)函數(shù)
        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;
    }

從上面函數(shù)執(zhí)行的語句中,可以看到起傳遞的參數(shù)有WebView,JSonObject和一個(gè)Callback。

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

這里用來給JS調(diào)用的Java方法,傳遞的值都是JsonObject的形式,Callback則是回調(diào)相應(yīng)的JS方法,當(dāng)我們的Java方法執(zhí)行完成之后,如果我們需要調(diào)用相應(yīng)的JS方法,我們可以通過callback提供的apply方法來傳遞一些數(shù)據(jù)。

    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方法的實(shí)現(xiàn)。

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);
           }
         });
     }
  }

這里將需要傳回的數(shù)據(jù),傳遞給JSBridge中的onFinish方法。傳遞的數(shù)據(jù)中有一個(gè)端口號(hào),通過這個(gè)端口號(hào)作為標(biāo)示,來調(diào)用相應(yīng)的方法。

這里方法注冊(cè)的方式會(huì)局限在某一些類中,而且需要我們提前對(duì)所有的方法進(jìn)行注冊(cè),同時(shí)對(duì)于實(shí)例方法和靜態(tài)方法,在執(zhí)行上也會(huì)有一些區(qū)分,對(duì)于方法的處理,可以通過兩端的協(xié)商,規(guī)定好調(diào)用的方法之后,可以設(shè)置一個(gè)處理類,然后對(duì)于每一個(gè)持有WebView的Activity,為其設(shè)置一個(gè)回調(diào),當(dāng)JS對(duì)傳輸?shù)膮?shù)進(jìn)行解析的時(shí)候,解析返回的數(shù)據(jù),進(jìn)行相應(yīng)的處理即可。這樣就可以調(diào)用到我們當(dāng)前的類,但是對(duì)于方法調(diào)用部分就需要我們手動(dòng)去處理,而非在JSBridge中作為一個(gè)黑盒處理。

  • 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文件提供了兩個(gè)方法,一個(gè)是call一個(gè)是finish,分別是web中被調(diào)用,另一個(gè)是在native中將會(huì)被調(diào)用,每一個(gè)web中調(diào)用我們native方法的時(shí)候,都會(huì)調(diào)用js文件中的oncall方法,同時(shí)也會(huì)傳遞一個(gè)函數(shù)作為回調(diào),js文件中會(huì)為該次調(diào)用隨機(jī)生成一個(gè)端口號(hào),同時(shí)將其回調(diào)保存在一個(gè)內(nèi)部列表callbacks中,根據(jù)協(xié)議規(guī)則,通過window.promt的方式將相應(yīng)的調(diào)用傳遞下去。

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

在WebChromeClient的onJsPrompt方法中便可以得到相應(yīng)的信息,通過JSBridge方法對(duì)回傳數(shù)據(jù)進(jìn)行相應(yīng)的調(diào)用。

JS中調(diào)用Java方法的方式

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

第一,二個(gè)參數(shù)表示調(diào)用的Java的方法,第三個(gè)參數(shù)為傳遞的參數(shù)。第四個(gè)參數(shù)為設(shè)置的回調(diào)函數(shù)。

至此,一個(gè)簡單的JSBridge實(shí)現(xiàn)了,JS只需要通過call方法傳遞相應(yīng)的參數(shù)即可調(diào)用Java方法,Java只需要將待調(diào)用方法進(jìn)行注冊(cè)即可。

交互中的安全漏洞問題

進(jìn)幾年和WebView遠(yuǎn)程代碼執(zhí)行相關(guān)的漏洞主要有CVE-2012-6336,CVE-2014-1939,CVE-2014-7224, 這些漏洞中最核心的漏洞是CVE-2012-6336,另外兩個(gè)CVE只是發(fā)現(xiàn)了幾個(gè)默認(rèn)存在的接口。

  • CVE-2012-6636

Android API 16.0及之前的版本中存在安全漏洞,該漏洞源于程序沒有正確限制使用WebView.addJavascriptInterface方法。遠(yuǎn)程攻擊者可通過使用Java Reflection API利用該漏洞執(zhí)行任意Java對(duì)象的方法

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

  • CVE-2014-1939

java/android/webkit/BrowserFrame.java 使用addJavascriptInterface API并創(chuàng)建了SearchBoxImpl類的對(duì)象。攻擊者可通過訪問searchBoxJavaBridge_接口利用該漏洞執(zhí)行任意Java代碼。

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

  • CVE-2014-7224

香港理工大學(xué)的研究人員發(fā)現(xiàn)當(dāng)系統(tǒng)輔助功能中的任意一項(xiàng)服務(wù)被開啟后,所有由系統(tǒng)提供的WebView都會(huì)被加入兩個(gè)JS objects,分別為是accessibility和accessibilityTraversal。惡意攻擊者就可以使用accessibility和accessibilityTraversal這兩個(gè)Java Bridge來執(zhí)行遠(yuǎn)程攻擊代碼.

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

對(duì)于上述漏洞,其攻擊原理為得到了Java對(duì)象,通過反射的方式來執(zhí)行自己的惡意代碼。

解決方案

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.升級(jí)系統(tǒng)API level 17后,只有顯示添加 @JavascriptInterface的方法才能被JavaScript調(diào)用,這樣反射就失去作用了。但對(duì)于更低版本則還是會(huì)存在,考慮采用其它方案,例如JSBridge實(shí)現(xiàn)交互。

參考資料

JsBridge 實(shí)現(xiàn) JavaScript 和 Java 的互相調(diào)用

Android:你要的WebView與 JS 交互方式 都在這里

Android WebView遠(yuǎn)程執(zhí)行代碼漏洞淺析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容