隨著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");
- 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);
}
- JS文件,向控制臺輸出信息。
function consoleTest() {
console.log("showToast");
}
- 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;
}
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)。
- 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)用