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 關(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)

  1. js中鏈接如果未加協(xié)議疫诽,則默認(rèn)會(huì)以file:///android_asset/開頭涤浇,即上面的代碼url為:file:///android_asset/showToast
  2. 關(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)來的鏈接(即不做任何處理)蹄皱,自己看著辦。
  3. 建議兩個(gè) shouldOverrideUrlLoading 方法都重寫芯肤,讓目標(biāo)設(shè)備自動(dòng)匹配對(duì)應(yīng)的回調(diào)方法巷折。如果只重寫其中的一個(gè)方法會(huì)因?yàn)槟繕?biāo)平臺(tái)API版本的不同而找不到回調(diào)方法。
  4. 使用這種方式調(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)

  1. 使用這種方式調(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)行了檢查:

從警告信息中可以看出昼钻,在API<17時(shí)掸屡,JavaScript可以通過反射機(jī)制操作應(yīng)用,API>=17以后,對(duì)Android端提供的原生方法都需要加上@JavascriptInterface注解然评,從而修復(fù)了該漏洞仅财。但就現(xiàn)在市場(chǎng)上的系統(tǒng)版本來看,把minSdkVersion設(shè)置成17還是有點(diǎ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)該接口
}

說明

  1. 本庫(kù)無調(diào)用反饋箱玷,只是JSBridge交互方式的一個(gè)簡(jiǎn)單實(shí)現(xiàn)怨规。
  2. Js調(diào)用Android端代碼時(shí),Android端可在WebChromeClient的onJsPrompt()方法中或WebViewClient中的shouldOverrideUrlLoading()方法中接收消息锡足。
  3. 本庫(kù)源碼已上傳Github波丰,歡迎大家提交issue,后期會(huì)繼續(xù)更新更多功能舶得,如果有感興趣的小伙伴也可以和我一同維護(hù)這個(gè)庫(kù)掰烟。JSBridge
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子纫骑,更是在濱河造成了極大的恐慌蝎亚,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件惧磺,死亡現(xiàn)場(chǎng)離奇詭異颖对,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)磨隘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門缤底,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人番捂,你說我怎么就攤上這事个唧。” “怎么了设预?”我有些...
    開封第一講書人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵徙歼,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我鳖枕,道長(zhǎng)魄梯,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任宾符,我火速辦了婚禮酿秸,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘魏烫。我一直安慰自己辣苏,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開白布哄褒。 她就那樣靜靜地躺著稀蟋,像睡著了一般。 火紅的嫁衣襯著肌膚如雪呐赡。 梳的紋絲不亂的頭發(fā)上退客,一...
    開封第一講書人閱讀 49,749評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音链嘀,去河邊找鬼萌狂。 笑死,一個(gè)胖子當(dāng)著我的面吹牛管闷,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播窃肠,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼包个,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起碧囊,我...
    開封第一講書人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤树灶,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后糯而,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體天通,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年熄驼,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了像寒。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡瓜贾,死狀恐怖诺祸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情祭芦,我是刑警寧澤筷笨,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站龟劲,受9級(jí)特大地震影響胃夏,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜昌跌,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一仰禀、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧避矢,春花似錦悼瘾、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至砂沛,卻和暖如春烫扼,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背碍庵。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來泰國(guó)打工映企, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人静浴。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓堰氓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親苹享。 傳聞我的和親對(duì)象是個(gè)殘疾皇子双絮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容