Android原生與Html交互方式
Java調(diào)用Js
方式1
WebView wv = new WebView(getAppclicationContext());
wv.getSettings().setJavaScriptEnabled(true);
wv.loadUrl("javascript:funcName()");
方式2(API >= 19)
WebView wv = new WebView(getAppclicationContext());
wv.getSettings().setJavaScriptEnabled(true);
// API 19(4.4) 添加的方法,onReceiveValue 回調(diào)方法的參數(shù)值為Js函數(shù)的返回值,此方法必須在UI線程中調(diào)用
wv.evaluateJavascript("javascript:javaCallJSNoArgsFunc()", new ValueCallback<String>() {
@Override
public void onReceiveValue(String s) {
}
});
Js調(diào)用Java
方式1(系統(tǒng)提供方式)
WebView wv = new WebView(getAppclicationContext());
wv.getSettings().setJavaScriptEnabled(true);
wv.addJavascriptInterface(new JavaInterface(),"Android");
方式2(shouldOverrideUrlLoading)
// js 關鍵代碼
<a href="showToast">showToast</a>
// Java 代碼
WebView wv = new WebView(getAppclicationContext());
wv.setWebViewClient(new WebViewClient(){
// API 24 added this method
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
if ("showToast".equals(request.getUrl().replace("file:///android_asset/","")))
Toast.makeText(MainAty.this,"NativeToast",Toast.LENGTH_SHORT).show();
return false;
}
// API 24 deprecated this method
// url格式為:file:///android_asset/showToast
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
System.out.println("url="+url);
if ("showToast".equals(url.replace("file:///android_asset/","")))
Toast.makeText(MainAty.this,"NativeToast",Toast.LENGTH_SHORT).show();
return true;
}
});
使用方式2注意點:
- js中鏈接如果未加協(xié)議,則默認會以file:///android_asset/開頭,即上面的代碼url為:file:///android_asset/showToast
- 關于shouldOverrideUrlLoading這個方法的返回值,true表示當前WebView會加載這個傳入進來的鏈接,如果這個鏈接地址有誤,會展示錯誤網(wǎng)頁;false表示當前WebView不會加載這個傳入進來的鏈接(即不做任何處理),自己看著辦。
- 建議兩個 shouldOverrideUrlLoading 方法都重寫,讓目標設備自動匹配對應的回調(diào)方法。如果只重寫其中的一個方法會因為目標平臺API版本的不同而找不到回調(diào)方法。
- 使用這種方式調(diào)用Java代碼,Android 端不用設置 wv.getSettings().setJavaScriptEnabled(true);wv.addJavascriptInterface(new InteractionObj(),"android"),相對比較安全。
方式3(WebChromeClient)
WebView wv = new WebView(getApplicationContext());
wv.getSettings().setJavaScriptEnabled(true);
wv.setWebChromeClient(new WebChromeClient(){
// 對應Js alert() 函數(shù)
// @return true 表示客戶端自己處理彈出框事件,alert()函數(shù)會失效,即不會有對話框彈出。
此時必須調(diào)用result.confirm()或result.cancel()來返回結(jié)果,不然html頁面將無法操作。
false 正常彈出對話框
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
System.out.println("chrome alert");
return super.onJsAlert(view, url, message, result);
}
// 對應Js confirm() 函數(shù)
// @return true 表示客戶端自己處理彈出框事件,confirm()函數(shù)會失效,即不會有對話框彈出。
此時必須調(diào)用result.confirm()或result.cancel()來返回結(jié)果,不然html頁面將無法操作。
false 正常彈出對話框
@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
System.out.println("chrome onJsConfirm");
// 返回結(jié)果:是
result.confirm();
// 返回結(jié)果:否
result.cancel();
return super.onJsConfirm(view, url, message, result);
}
// 對應Js console.log() 函數(shù)
// @return true 表示客戶端自己處理log消息,web端console.log()函數(shù)將會失效,即不會有l(wèi)og信息輸出。
false web端會接著處理這個log消息,即有l(wèi)og信息打印
@Override
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
System.out.println("chrome onConsoleMessage");
return super.onConsoleMessage(consoleMessage);
}
// 對應Js prompt() 函數(shù)
// Js 使用最少的函數(shù),建議用此回調(diào)方法
// @return true 表示客戶端自己處理彈出框事件,prompt()函數(shù)會失效,即不會有對話框彈出。
此時必須調(diào)用result.confirm()或result.cancel()來返回結(jié)果,不然html頁面將無法操作。
false 正常彈出對話框
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
System.out.println("chrome onJsPrompt");
// 參數(shù)為 var r = prompt() 的返回值,即r="android"
result.confirm("android");
return super.onJsPrompt(view, url, message, defaultValue, result);
}
});
使用方式3注意點:
- 使用這種方式調(diào)用Java代碼,Android 端不用設置 wv.addJavascriptInterface(new InteractionObj(),"android"),但是必須設置wv.getSettings().setJavaScriptEnabled(true);相對比較安全。
交互時存在的漏洞
跨站點腳本攻擊(XSS)
漏洞出現(xiàn)前提
只要Android端設置了 wv.getSettings().setJavaScriptEnabled(true),就存在一個跨站點腳本攻擊漏洞。在Android Studio編輯器中也對該漏洞進行了檢查:

遠程執(zhí)行Android端任意原生代碼
漏洞出現(xiàn)前提
Android往Web頁面注入了Java實例對象,即調(diào)用了:wv.addJavascriptInterface(new InteractionObj(),"android")。在Android Studio編輯器中也對該漏洞進行了檢查:

Js 惡意代碼
<!--Web端會利用Android端提供的原生實例對象,利用Java反射機制執(zhí)行任意Android原生代碼-->
function illegalInvokeJavaMethod(android){
<!--網(wǎng)上有資料說forName()只能調(diào)用系統(tǒng)類提供的API,而loadClass()方法能調(diào)用任意類提供的API。但我自己寫了個類測試后,發(fā)現(xiàn)后者并不能對任意類API進行調(diào)用,我是用的4.0的系統(tǒng)提供模擬器進行測試,不知道是否與具體機型有關,希望大神指點-->
var clz = android.getClass().getClassLoader().loadClass("cn.demo.jsinteraction.WebViewBugClass");
clz.getDeclaredMethod("sout").invoke(clz.newInstance());
}
JsBridge
為什么要使用JsBridge
Android 4.2 之前,Web端如果使用系統(tǒng)提供的方式(見上文Js調(diào)用Java方式1)調(diào)用Android端原生方法時,Android WebView存在一個JavaScript可以利用Android端提供的原生實例對象,并利用Java反射機制執(zhí)行任意Android端原生代碼的安全漏洞,雖然此漏洞在Android 4.2以后得到了解決,但由于版本兼容性問題,基本上不會用這種方式實現(xiàn)交互。為了保證交互時的安全性及開發(fā)的便利性,則需要用到JsBridge交互方式。此外,大家?guī)缀趺刻於家褂玫奈⑿拧⒅Ц秾?、QQ等都在使用JsBridge,只是他們封裝的JsBridge功能將更為強大。
簡單JsBridge庫實現(xiàn)
JSBridge類
public class JSBridge {
// 緩存暴露類的所有方法
private static Map<String, Map<String, Method>> exposedMethods = new HashMap<>();
/**
* 調(diào)用JavaScript函數(shù)
*
* @param webView current WebView
* @param func target JavaScript function
* @param params the target function parameters
*/
public static void callJSFunc(WebView webView, String func, String... params) {
if (webView == null || func == null || "".equals(func) || params == null)
throw new RuntimeException("callJSFunc method exist illegal parameter");
StringBuilder sb = new StringBuilder("javascript:" + func + "(");
if (params.length > 0) {
for (String param : params) {
sb.append("\'");
sb.append(param);
sb.append("\'");
sb.append(",");
}
sb.replace(sb.length() - 1, sb.length(), "");
}
sb.append(")");
webView.loadUrl(sb.toString());
}
/**
* 調(diào)用Java方法
* web端傳來的消息格式:jsbridge://className/methodName?{\"param1\":\"value1\",\"param2\":\"value2\"}
* web端參數(shù)定義格式:var msg = "{\"msg\":\"msg from javascript\"}"
*
* @param className the register class name
* @param methodName target method
* @param params the method args,if this parameter is not passed,its length is 0,not null
* @return method return value
*/
public static Object callJavaMethod(String className, String methodName, Object... params) {
if (className == null || "".equals(className) || methodName == null || "".equals(methodName))
throw new RuntimeException("callJavaMethod method exist illegal parameter");
if (!exposedMethods.containsKey(className))
throw new RuntimeException(className + " class not register");
if (!exposedMethods.get(className).containsKey(methodName))
throw new RuntimeException(methodName + "the invoked method dose not exist");
Method method = exposedMethods.get(className).get(methodName);
try {
return method.invoke(null, params);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
/**
* 注冊需要顯露給web端的Java類,獲取類中所有方法并緩存
*
* @param className the class need exposed
* @param clz the exposed class Class instance
*/
public static void register(String className, Class<? extends IBridge> clz) {
if (className == null || "".equals(className) || clz == null)
throw new RuntimeException("register method exist illegal parameter");
if (!exposedMethods.containsKey(className)) {
Map<String, Method> methods = new HashMap<>();
for (Method method : clz.getMethods()) {
methods.put(method.getName(), method);
}
exposedMethods.put(className, methods);
}
}
/**
* 獲取 String strJs = jsbridge://className/methodName?{\"param1\":\"value1\"} 中的 className
*
* @param uri Uri uri = Uri.parse(strJs)
* @return the className in strJs
*/
public static String getClassName(Uri uri){
if (uri == null)
throw new RuntimeException("getClassName method parameter is null");
String className = uri.getHost();
return className == null || "".equals(className) ? "" : className;
}
/**
* 獲取 String strJs = jsbridge://className/methodName?{\"param1\":\"value1\"} 中的 methodName
*
* @param uri Uri uri = Uri.parse(strJs)
* @return the methodName in strJs
*/
public static String getMethodName(Uri uri){
if (uri == null)
throw new RuntimeException("getMethodName method parameter is null");
String methodName = uri.getPath().replace("/","");
return methodName == null || "".equals(methodName) ? "" : methodName;
}
/**
* 獲取 String strJs = jsbridge://className/methodName?{\"param1\":\"value1\"} 中的 {\"param1\":\"value1\"}
*
* @param uri Uri uri = Uri.parse(strJs)
* @return the Json parameter in strJs
*/
public static JSONObject getMethodJsonParams(Uri uri){
if (uri == null)
throw new RuntimeException("getMethodJsonParams method parameter is null");
String queryStrJSON = uri.getQuery();
if (queryStrJSON == null || "".equals(queryStrJSON) || "{}".equals(queryStrJSON))
return null;
try {
return new JSONObject(queryStrJSON);
} catch (JSONException e) {
e.printStackTrace();
}
return null;
}
}
IBridge 標識接口
public interface IBridge {
// 暴露給web端的類必須實現(xiàn)該接口
}
說明
- 本庫無調(diào)用反饋,只是JSBridge交互方式的一個簡單實現(xiàn)。
- Js調(diào)用Android端代碼時,Android端可在WebChromeClient的onJsPrompt()方法中或WebViewClient中的shouldOverrideUrlLoading()方法中接收消息。
- 本庫源碼已上傳Github,歡迎大家提交issue,后期會繼續(xù)更新更多功能,如果有感興趣的小伙伴也可以和我一同維護這個庫。JSBridge