Android原生与Html交互方式
Java调用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 回调方法的参数值为Js函数的返回值,此方法必须在UI线程中调用
wv.evaluateJavascript("javascript:javaCallJSNoArgsFunc()", new ValueCallback() {
@Override
public void onReceiveValue(String s) {
}
});
Js调用Java
方式1(系统提供方式)
WebView wv = new WebView(getAppclicationContext());
wv.getSettings().setJavaScriptEnabled(true);
wv.addJavascriptInterface(new JavaInterface(),"Android");
方式2(shouldOverrideUrlLoading)
// js 关键代码
showToast
// 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中链接如果未加协议,则默认会以file:///android_asset/开头,即上面的代码url为:file:///android_asset/showToast
- 关于shouldOverrideUrlLoading这个方法的返回值,true表示当前WebView会加载这个传入进来的链接,如果这个链接地址有误,会展示错误网页;false表示当前WebView不会加载这个传入进来的链接(即不做任何处理),自己看着办。
- 建议两个 shouldOverrideUrlLoading 方法都重写,让目标设备自动匹配对应的回调方法。如果只重写其中的一个方法会因为目标平台API版本的不同而找不到回调方法。
- 使用这种方式调用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() 函数
// @return true 表示客户端自己处理弹出框事件,alert()函数会失效,即不会有对话框弹出。
此时必须调用result.confirm()或result.cancel()来返回结果,不然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() 函数
// @return true 表示客户端自己处理弹出框事件,confirm()函数会失效,即不会有对话框弹出。
此时必须调用result.confirm()或result.cancel()来返回结果,不然html页面将无法操作。
false 正常弹出对话框
@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
System.out.println("chrome onJsConfirm");
// 返回结果:是
result.confirm();
// 返回结果:否
result.cancel();
return super.onJsConfirm(view, url, message, result);
}
// 对应Js console.log() 函数
// @return true 表示客户端自己处理log消息,web端console.log()函数将会失效,即不会有log信息输出。
false web端会接着处理这个log消息,即有log信息打印
@Override
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
System.out.println("chrome onConsoleMessage");
return super.onConsoleMessage(consoleMessage);
}
// 对应Js prompt() 函数
// Js 使用最少的函数,建议用此回调方法
// @return true 表示客户端自己处理弹出框事件,prompt()函数会失效,即不会有对话框弹出。
此时必须调用result.confirm()或result.cancel()来返回结果,不然html页面将无法操作。
false 正常弹出对话框
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
System.out.println("chrome onJsPrompt");
// 参数为 var r = prompt() 的返回值,即r="android"
result.confirm("android");
return super.onJsPrompt(view, url, message, defaultValue, result);
}
});
使用方式3注意点:
- 使用这种方式调用Java代码,Android 端不用设置 wv.addJavascriptInterface(new InteractionObj(),"android"),但是必须设置wv.getSettings().setJavaScriptEnabled(true);相对比较安全。
交互时存在的漏洞
跨站点脚本攻击(XSS)
漏洞出现前提
只要Android端设置了 wv.getSettings().setJavaScriptEnabled(true),就存在一个跨站点脚本攻击漏洞。在Android Studio编辑器中也对该漏洞进行了检查:
远程执行Android端任意原生代码
漏洞出现前提
Android往Web页面注入了Java实例对象,即调用了:wv.addJavascriptInterface(new InteractionObj(),"android")。在Android Studio编辑器中也对该漏洞进行了检查:
Js 恶意代码
function illegalInvokeJavaMethod(android){
var clz = android.getClass().getClassLoader().loadClass("cn.demo.jsinteraction.WebViewBugClass");
clz.getDeclaredMethod("sout").invoke(clz.newInstance());
}
JsBridge
为什么要使用JsBridge
Android 4.2 之前,Web端如果使用系统提供的方式(见上文Js调用Java方式1)调用Android端原生方法时,Android WebView存在一个JavaScript可以利用Android端提供的原生实例对象,并利用Java反射机制执行任意Android端原生代码的安全漏洞,虽然此漏洞在Android 4.2以后得到了解决,但由于版本兼容性问题,基本上不会用这种方式实现交互。为了保证交互时的安全性及开发的便利性,则需要用到JsBridge交互方式。此外,大家几乎每天都要使用的微信、支付宝、QQ等都在使用JsBridge,只是他们封装的JsBridge功能将更为强大。
简单JsBridge库实现
JSBridge类
public class JSBridge {
// 缓存暴露类的所有方法
private static Map> exposedMethods = new HashMap<>();
/**
* 调用JavaScript函数
*
* @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());
}
/**
* 调用Java方法
* web端传来的消息格式:jsbridge://className/methodName?{\"param1\":\"value1\",\"param2\":\"value2\"}
* web端参数定义格式: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 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端的类必须实现该接口
}
说明
- 本库无调用反馈,只是JSBridge交互方式的一个简单实现。
- Js调用Android端代码时,Android端可在WebChromeClient的onJsPrompt()方法中或WebViewClient中的shouldOverrideUrlLoading()方法中接收消息。
- 本库源码已上传Github,欢迎大家提交issue,后期会继续更新更多功能,如果有感兴趣的小伙伴也可以和我一同维护这个库。JSBridge