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 關(guān)鍵代碼
<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注意點(diǎn):
- js中鏈接如果未加協(xié)議疫诽,則默認(rèn)會(huì)以file:///android_asset/開頭涤浇,即上面的代碼url為:file:///android_asset/showToast
- 關(guān)于shouldOverrideUrlLoading這個(gè)方法的返回值,true表示當(dāng)前WebView會(huì)加載這個(gè)傳入進(jìn)來的鏈接,如果這個(gè)鏈接地址有誤,會(huì)展示錯(cuò)誤網(wǎng)頁(yè);false表示當(dāng)前WebView不會(huì)加載這個(gè)傳入進(jìn)來的鏈接(即不做任何處理)蹄皱,自己看著辦。
- 建議兩個(gè) shouldOverrideUrlLoading 方法都重寫芯肤,讓目標(biāo)設(shè)備自動(dòng)匹配對(duì)應(yīng)的回調(diào)方法巷折。如果只重寫其中的一個(gè)方法會(huì)因?yàn)槟繕?biāo)平臺(tái)API版本的不同而找不到回調(diào)方法。
- 使用這種方式調(diào)用Java代碼,Android 端不用設(shè)置 wv.getSettings().setJavaScriptEnabled(true);wv.addJavascriptInterface(new InteractionObj(),"android"),相對(duì)比較安全崖咨。
方式3(WebChromeClient)
WebView wv = new WebView(getApplicationContext());
wv.getSettings().setJavaScriptEnabled(true);
wv.setWebChromeClient(new WebChromeClient(){
// 對(duì)應(yīng)Js alert() 函數(shù)
// @return true 表示客戶端自己處理彈出框事件锻拘,alert()函數(shù)會(huì)失效,即不會(huì)有對(duì)話框彈出击蹲。
此時(shí)必須調(diào)用result.confirm()或result.cancel()來返回結(jié)果署拟,不然html頁(yè)面將無法操作。
false 正常彈出對(duì)話框
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
System.out.println("chrome alert");
return super.onJsAlert(view, url, message, result);
}
// 對(duì)應(yīng)Js confirm() 函數(shù)
// @return true 表示客戶端自己處理彈出框事件歌豺,confirm()函數(shù)會(huì)失效推穷,即不會(huì)有對(duì)話框彈出。
此時(shí)必須調(diào)用result.confirm()或result.cancel()來返回結(jié)果类咧,不然html頁(yè)面將無法操作馒铃。
false 正常彈出對(duì)話框
@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);
}
// 對(duì)應(yīng)Js console.log() 函數(shù)
// @return true 表示客戶端自己處理log消息,web端console.log()函數(shù)將會(huì)失效痕惋,即不會(huì)有l(wèi)og信息輸出骗露。
false web端會(huì)接著處理這個(gè)log消息,即有l(wèi)og信息打印
@Override
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
System.out.println("chrome onConsoleMessage");
return super.onConsoleMessage(consoleMessage);
}
// 對(duì)應(yīng)Js prompt() 函數(shù)
// Js 使用最少的函數(shù)血巍,建議用此回調(diào)方法
// @return true 表示客戶端自己處理彈出框事件,prompt()函數(shù)會(huì)失效珊随,即不會(huì)有對(duì)話框彈出述寡。
此時(shí)必須調(diào)用result.confirm()或result.cancel()來返回結(jié)果,不然html頁(yè)面將無法操作叶洞。
false 正常彈出對(duì)話框
@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ǎn):
- 使用這種方式調(diào)用Java代碼,Android 端不用設(shè)置 wv.addJavascriptInterface(new InteractionObj(),"android"),但是必須設(shè)置wv.getSettings().setJavaScriptEnabled(true);相對(duì)比較安全。
交互時(shí)存在的漏洞
跨站點(diǎn)腳本攻擊(XSS)
漏洞出現(xiàn)前提
只要Android端設(shè)置了 wv.getSettings().setJavaScriptEnabled(true)衩辟,就存在一個(gè)跨站點(diǎn)腳本攻擊漏洞螟炫。在Android Studio編輯器中也對(duì)該漏洞進(jìn)行了檢查:
遠(yuǎn)程執(zhí)行Android端任意原生代碼
漏洞出現(xiàn)前提
Android往Web頁(yè)面注入了Java實(shí)例對(duì)象,即調(diào)用了:wv.addJavascriptInterface(new InteractionObj(),"android")艺晴。在Android Studio編輯器中也對(duì)該漏洞進(jìn)行了檢查:
Js 惡意代碼
<!--Web端會(huì)利用Android端提供的原生實(shí)例對(duì)象盏求,利用Java反射機(jī)制執(zhí)行任意Android原生代碼-->
function illegalInvokeJavaMethod(android){
<!--網(wǎng)上有資料說forName()只能調(diào)用系統(tǒng)類提供的API,而loadClass()方法能調(diào)用任意類提供的API亿眠。但我自己寫了個(gè)類測(cè)試后碎罚,發(fā)現(xiàn)后者并不能對(duì)任意類API進(jìn)行調(diào)用,我是用的4.0的系統(tǒng)提供模擬器進(jìn)行測(cè)試纳像,不知道是否與具體機(jī)型有關(guān)荆烈,希望大神指點(diǎn)-->
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端原生方法時(shí)爹耗,Android WebView存在一個(gè)JavaScript可以利用Android端提供的原生實(shí)例對(duì)象耙考,并利用Java反射機(jī)制執(zhí)行任意Android端原生代碼的安全漏洞,雖然此漏洞在Android 4.2以后得到了解決潭兽,但由于版本兼容性問題倦始,基本上不會(huì)用這種方式實(shí)現(xiàn)交互。為了保證交互時(shí)的安全性及開發(fā)的便利性山卦,則需要用到JsBridge交互方式鞋邑。此外,大家?guī)缀趺刻於家褂玫奈⑿耪巳亍⒅Ц秾毭锻搿Q等都在使用JsBridge,只是他們封裝的JsBridge功能將更為強(qiáng)大铸本。
簡(jiǎn)單JsBridge庫(kù)實(shí)現(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;
}
/**
* 注冊(cè)需要顯露給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 標(biāo)識(shí)接口
public interface IBridge {
// 暴露給web端的類必須實(shí)現(xiàn)該接口
}
說明
- 本庫(kù)無調(diào)用反饋箱玷,只是JSBridge交互方式的一個(gè)簡(jiǎn)單實(shí)現(xiàn)怨规。
- Js調(diào)用Android端代碼時(shí),Android端可在WebChromeClient的onJsPrompt()方法中或WebViewClient中的shouldOverrideUrlLoading()方法中接收消息锡足。
- 本庫(kù)源碼已上傳Github波丰,歡迎大家提交issue,后期會(huì)繼續(xù)更新更多功能舶得,如果有感興趣的小伙伴也可以和我一同維護(hù)這個(gè)庫(kù)掰烟。JSBridge