Android中Java和JS的交互

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

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

WebView的使用

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

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

Android中 JS和Java的交互方式

在進行交互之前需要我們對WebView進行設置開啟對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(),攔截控制臺信息
    • 通過WebChromeClient.onJsPrompt(),onJsAlert()、onJsConfirm()攔截Web相應彈框的事件

Java調(diào)用JS

在Java中調(diào)用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來調(diào)用JS方法
mWebView.post(new Runnable() {
            @Override
            public void run() {
                mWebView.loadUrl("javascript:alertTest()");
            }
        });
  • 通過evaluateJavascript來調(diào)用JS方法

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

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

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

JS調(diào)用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調(diào)用");
 }
測試
  • shouldOverrideUrlLoading

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

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

1.在WebChromeClient中定義相應回調(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文件,向控制臺輸出信息。
 function consoleTest() {
     console.log("showToast");
 }
demo展示
  • onJsPrompt,onJsConfirm, onJsAlert

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

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

1.JS文件

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

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

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

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

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

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

對于JS可能會調(diào)用到的Java方法,進行集中管理,對于每一個類,可以自定義名稱,方便在JS中的調(diào)用。可能會被調(diào)用到的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();
            }
        }
    }
      ....
}

除此之外,還提供了一個調(diào)用函數(shù),這個函數(shù)主要是對傳遞的數(shù)據(jù)根據(jù)我們制定的協(xié)議進行解析,然后從注冊的函數(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í)行相應函數(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和一個Callback。

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

這里用來給JS調(diào)用的Java方法,傳遞的值都是JsonObject的形式,Callback則是回調(diào)相應的JS方法,當我們的Java方法執(zhí)行完成之后,如果我們需要調(diào)用相應的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方法的實現(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ù)中有一個端口號,通過這個端口號作為標示,來調(diào)用相應的方法。

這里方法注冊的方式會局限在某一些類中,而且需要我們提前對所有的方法進行注冊,同時對于實例方法和靜態(tài)方法,在執(zhí)行上也會有一些區(qū)分,對于方法的處理,可以通過兩端的協(xié)商,規(guī)定好調(diào)用的方法之后,可以設置一個處理類,然后對于每一個持有WebView的Activity,為其設置一個回調(diào),當JS對傳輸?shù)膮?shù)進行解析的時候,解析返回的數(shù)據(jù),進行相應的處理即可。這樣就可以調(diào)用到我們當前的類,但是對于方法調(diào)用部分就需要我們手動去處理,而非在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中被調(diào)用,另一個是在native中將會被調(diào)用,每一個web中調(diào)用我們native方法的時候,都會調(diào)用js文件中的oncall方法,同時也會傳遞一個函數(shù)作為回調(diào),js文件中會為該次調(diào)用隨機生成一個端口號,同時將其回調(diào)保存在一個內(nèi)部列表callbacks中,根據(jù)協(xié)議規(guī)則,通過window.promt的方式將相應的調(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方法中便可以得到相應的信息,通過JSBridge方法對回傳數(shù)據(jù)進行相應的調(diào)用。

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

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

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

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

交互中的安全漏洞問題

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

  • CVE-2012-6636

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

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

  • CVE-2014-1939

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

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

  • CVE-2014-7224

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

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

對于上述漏洞,其攻擊原理為得到了Java對象,通過反射的方式來執(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.升級系統(tǒng)API level 17后,只有顯示添加 @JavascriptInterface的方法才能被JavaScript調(diào)用,這樣反射就失去作用了。但對于更低版本則還是會存在,考慮采用其它方案,例如JSBridge實現(xiàn)交互。

參考資料

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

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

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

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內(nèi)容