Android WebView 安全性

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注意點

  1. js中鏈接如果未加協(xié)議,則默認會以file:///android_asset/開頭,即上面的代碼url為:file:///android_asset/showToast
  2. 關于shouldOverrideUrlLoading這個方法的返回值,true表示當前WebView會加載這個傳入進來的鏈接,如果這個鏈接地址有誤,會展示錯誤網(wǎng)頁;false表示當前WebView不會加載這個傳入進來的鏈接(即不做任何處理),自己看著辦。
  3. 建議兩個 shouldOverrideUrlLoading 方法都重寫,讓目標設備自動匹配對應的回調(diào)方法。如果只重寫其中的一個方法會因為目標平臺API版本的不同而找不到回調(diào)方法。
  4. 使用這種方式調(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注意點

  1. 使用這種方式調(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編輯器中也對該漏洞進行了檢查:

從警告信息中可以看出,在API<17時,JavaScript可以通過反射機制操作應用,API>=17以后,對Android端提供的原生方法都需要加上@JavascriptInterface注解,從而修復了該漏洞。但就現(xiàn)在市場上的系統(tǒng)版本來看,把minSdkVersion設置成17還是有點不妥。

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)該接口
}

說明

  1. 本庫無調(diào)用反饋,只是JSBridge交互方式的一個簡單實現(xiàn)。
  2. Js調(diào)用Android端代碼時,Android端可在WebChromeClient的onJsPrompt()方法中或WebViewClient中的shouldOverrideUrlLoading()方法中接收消息。
  3. 本庫源碼已上傳Github,歡迎大家提交issue,后期會繼續(xù)更新更多功能,如果有感興趣的小伙伴也可以和我一同維護這個庫。JSBridge
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容