webview加載https鏈接(終極解決方案,包括https鏈接加載http圖片顯示失敗)

前言:只要涉及到https,大家都會(huì)第一時(shí)間想到證書驗(yàn)證。當(dāng)然,這是沒問題的。如果有要求,這個(gè)證書驗(yàn)證是必須的。一般情況下,都是需要證書的(有證書畢竟安全些嗎)

正常的解決辦法:

重寫類WebViewClient
第一種方法:

webView.setWebViewClient(new WebViewClient ()
                @Override
                public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
                    super.onReceivedSslError(view, handler, error);
                }
            });

說明:Android WebView組件加載網(wǎng)頁發(fā)生證書認(rèn)證錯(cuò)誤時(shí),會(huì)調(diào)用WebViewClient類的onReceivedSslError方法,在這個(gè)方法里,我們可以點(diǎn)擊源碼看到SslErrorHandler中有兩個(gè)主要的方法可以調(diào)用

【1】cancel( )
停止加載問題頁面

【2】proceed( )
忽略SSL證書錯(cuò)誤,繼續(xù)加載頁面

如果我們不考慮證書安全,則可以直接這樣寫

webView.setWebViewClient(new WebViewClient ()
                @Override
                public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
                    handler.proceed();
                }
            });

當(dāng)然了,既然用到了https,我們大部分時(shí)候都是為了網(wǎng)絡(luò)安全問題
所以,可以定義一個(gè)類來驗(yàn)證證書安全,代碼如下:

/**
 * 時(shí)間 :2018/1/11  10:17
 * 作者 :陳奇
 * 作用 :證書驗(yàn)證的方法
 */
public class CertifiUtils {
    // 驗(yàn)證證書
    public static void OnCertificateOfVerification(final SslErrorHandler handler, String url) {
        OkHttpClient.Builder builder = setCertificates(new OkHttpClient.Builder());
        Request request = new Request.Builder().url(url)
                .build();
        builder.build().newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                BNLog.e("證書驗(yàn)證失敗", e.getMessage());
                handler.cancel();
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                BNLog.e("證書驗(yàn)證成功", response.body().string());
                handler.proceed();
            }
        });
    }

    private static OkHttpClient.Builder setCertificates(OkHttpClient.Builder client) {
        try {
            //Xutils.getSSLContext:獲取證書SSLSocketFactory(這個(gè)網(wǎng)絡(luò)上有很多代碼,并且我之前的文章里也有寫出來,在這里就不過多的描述了)
            SSLSocketFactory sslSocketFactory = Xutils.getSSLContext(BestnetApplication.contextApplication).getSocketFactory();
            X509TrustManager trustManager = Platform.get().trustManager(sslSocketFactory);
            client.sslSocketFactory(sslSocketFactory, trustManager);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return client;
    }
}

onReceivedSslError中的代碼就可以這么寫

webView.setWebViewClient(new WebViewClient ()
                @Override
                public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
                    CertifiUtils.OnCertificateOfVerification(handler,view.getUrl() );
                }
            });

但是,還有種情況是這樣的,當(dāng)webview加載url時(shí),它并不調(diào)用onReceivedSslError方法,而是在onReceivedError方法里返回net::ERR_SSL_PROTOCOL_ERROR。原因呢,當(dāng)時(shí)我調(diào)用是我們的鏈接,而這個(gè)鏈接則請(qǐng)求的是第三方請(qǐng)求,但是這個(gè)第三方請(qǐng)求必須要驗(yàn)證證書,這個(gè)時(shí)候如果沒有驗(yàn)證證書,第三方請(qǐng)求就會(huì)返回一個(gè)錯(cuò)誤給我們的鏈接,然后在返回到客戶端。也就是說第三方直接否定了這次請(qǐng)求,這個(gè)請(qǐng)求呢,就不是我們的鏈接能控制的了。所以這個(gè)時(shí)候是調(diào)用的是onReceivedError,而不調(diào)用onReceivedSslError。只是,最根本的原理是,webview加載html資源,默認(rèn)支持的是http請(qǐng)求而不是https請(qǐng)求。所以我們是不是只要把http請(qǐng)求替換成帶驗(yàn)證證書的https請(qǐng)求就好了?當(dāng)然,通過證明,這是可行的。

在WebViewClient 類中就提供了攔截的方法shouldInterceptRequest

shouldInterceptRequest有兩種重載。
從API 11開始引入,API 21棄用
public WebResourceResponse shouldInterceptRequest (WebView view, String url)

從API 21開始引入
public WebResourceResponse shouldInterceptRequest (WebView view, WebResourceRequest request)

綜上所述,代碼如下:

/**
 * 時(shí)間 :2018/1/19  18:24
 * 作者 :陳奇
 * 作用 :自定義webViewClient,攔截http(s)請(qǐng)求,并對(duì)相應(yīng)的加載證書驗(yàn)證
 */
public class SslPinningWebViewClient extends WebViewClient {

    private SSLContext sslContext;

    public SslPinningWebViewClient() throws IOException {
        // 從封裝好的方法中獲取加載了證書的SSLContext
        sslContext = Xutils.getSSLContext(BestnetApplication.contextApplication);
    }

    @Override
    public WebResourceResponse shouldInterceptRequest(final WebView view, String url) {
        return processRequest(Uri.parse(url));
    }

    @Override
    @TargetApi(21)
    public WebResourceResponse shouldInterceptRequest(final WebView view, WebResourceRequest interceptedRequest) {
        return processRequest(interceptedRequest.getUrl());
    }

    private WebResourceResponse processRequest(Uri uri) {
        Log.d("SSL_PINNING_WEBVIEWS", "GET: " + uri.toString());

        try {
            // 定義獲取的資源流,資源類型,資源編碼
            InputStream is;
            String contentType;
            String encoding;
            // 設(shè)置一個(gè)url鏈接
            URL url = new URL(uri.toString());
            // 如果是http請(qǐng)求,這里如果加載的是http請(qǐng)求,則也要建立HttpURLConnection而不是默認(rèn)加載。
            if (uri.toString().startsWith(APPConstants.CONFIGURATION_HTTP)) {
                HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
                is = httpURLConnection.getInputStream();
                contentType = httpURLConnection.getContentType();
                encoding = httpURLConnection.getContentEncoding();
            } else { // 如果是https請(qǐng)求
                HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
                urlConnection.setSSLSocketFactory(sslContext.getSocketFactory());
                is = urlConnection.getInputStream();
                contentType = urlConnection.getContentType();
                encoding = urlConnection.getContentEncoding();
            }

            // 如果信息頭中的資源類型不為null,則繼續(xù)
            if (contentType != null) {

                String mimeType = contentType;

                // 解析出mimeType
                if (contentType.contains(";")) {
                    mimeType = contentType.split(";")[0].trim();
                }

                Log.d("SSL_PINNING_WEBVIEWS", "Mime: " + mimeType);

                // 返回設(shè)置重新設(shè)置過的請(qǐng)求
                return new WebResourceResponse(mimeType, encoding, is);
            }

        } catch (SSLHandshakeException e) {
            e.printStackTrace();
            Log.d("SSL_PINNING_WEBVIEWS", e.getLocalizedMessage());
        } catch (Exception e) {
            e.printStackTrace();
            Log.d("SSL_PINNING_WEBVIEWS", e.getLocalizedMessage());
        }

        // 返回一個(gè)空的請(qǐng)求
        return new WebResourceResponse(null, null, null);
    }

    private boolean isCause(Class<? extends Throwable> expected, Throwable exc) {
        return expected.isInstance(exc) || (exc != null && isCause(expected, exc.getCause()));
    }
}

https加載http圖片顯示不出來:
1、設(shè)置getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);

2、或者使用上面的SslPinningWebViewClient

使用時(shí)是需要調(diào)用就可以了
webView.setWebViewClient(new SslPinningWebViewClient() {})

缺點(diǎn):這種重寫,雖然可以加載出https和http,但是如果這個(gè)h5頁面中含有post帶參請(qǐng)求,在攔截的時(shí)候是攔截不出來參數(shù)的。這就導(dǎo)致了h5中有的地方點(diǎn)擊是有問題?。?!

第二種方案:

/**
 * 時(shí)間 :2018/1/25  14:48
 * 作者 :陳奇
 * 作用 :自定義webViewClient,加載https請(qǐng)求
 */
public class BasicWebViewClientEx extends WebViewClient {
    private X509Certificate[] certificatesChain;
    private PrivateKey clientCertPrivateKey;
    public BasicWebViewClientEx() {
        initPrivateKeyAndX509Certificate(BestnetApplication.contextApplication);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    @Override
    public void onReceivedClientCertRequest(WebView view, ClientCertRequest request) {
        if ((null != clientCertPrivateKey) && ((null != certificatesChain) && (certificatesChain.length != 0))) {
            request.proceed(clientCertPrivateKey, certificatesChain);
        } else {
            request.cancel();
        }
    }

    private static final String KEY_STORE_CLIENT_PATH = "client.p12";//客戶端要給服務(wù)器端認(rèn)證的證書
    private static final String KEY_STORE_PASSWORD = "btydbg2018";// 客戶端證書密碼

    private void initPrivateKeyAndX509Certificate(Context context) {
        try {
            KeyStore keyStore = KeyStore.getInstance("PKCS12");
            InputStream ksIn = context.getResources().getAssets().open(KEY_STORE_CLIENT_PATH);
            keyStore.load(ksIn, KEY_STORE_PASSWORD.toCharArray());
            Enumeration<?> localEnumeration;
            localEnumeration = keyStore.aliases();
            while (localEnumeration.hasMoreElements()) {
                String str3 = (String) localEnumeration.nextElement();
                clientCertPrivateKey = (PrivateKey) keyStore.getKey(str3, KEY_STORE_PASSWORD.toCharArray());
                if (clientCertPrivateKey == null) {
                    continue;
                } else {
                    Certificate[] arrayOfCertificate = keyStore.getCertificateChain(str3);
                    certificatesChain = new X509Certificate[arrayOfCertificate.length];
                    for (int j = 0; j < certificatesChain.length; j++) {
                        certificatesChain[j] = ((X509Certificate) arrayOfCertificate[j]);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onReceivedSslError(final WebView view, SslErrorHandler handler,
                                   SslError error) {
        handler.proceed();
    }

}

缺點(diǎn):只支持API21及以上的。在網(wǎng)上也有一些說是可以導(dǎo)入安卓4.2的源碼架包,然后作為系統(tǒng)架包依賴進(jìn)去,可是這樣,4.2系統(tǒng)以上的新的API就是用不了了。

此文只供參考,如有哪位大神有完美的解決方案,請(qǐng)艾特我,感謝!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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