WebView域名劫持問題以及自簽名證書驗證方式

一,背景介紹

WebView儘管有着各種各樣的問題,但是至今爲止很多前端頁面加載到客戶端還是要依靠WebView去加載,WebView加載的鏈接可以是不安全不需要經過證書認證的http,ws協議的域名,當然爲了數據的安全性,大部分網頁還是會採取https和wss協議的域名,在使用https或wss協議的域名過程中,就會出現域名劫持問題,導致用戶無法正常訪問,客戶端會在Webview的回調函數中收到sslError,一般來說這時候我們會向服務器驗證證書的合法性,也就是自簽名證書的驗證,驗證通過可以忽略證書錯誤,驗證不通過彈窗提示用戶證書錯誤。舉個例子,新疆地區有一些域名就會被攔截,這些域名可能在其他地區是正常的,導致用戶無法正常查看網頁。

二,https,wss與ssl的關係

1.https和wss都是基於ssl的加密安全協議
2.SSL(Secure Socket Layer,安全套接層) 簡單來說是一種加密技術, 通過它, 我們可以在通信的雙方上建立一個安全的通信鏈路, 因此數據交互的雙方可以安全地通信, 而不需要擔心數據被竊取。
3.總的來說,SSL就是基礎,在SSL上運行Websocket就是WSS,在SSL上運行http就是https。

三,域名劫持的解決方案?

1.既然域名被劫持了,那我直接更換域名就ok了。更換的方式有2種,第一種是服務端全部更換,這樣做是不好的,因爲正常地區這個域名是未被劫持的,更好的方案是根據ip或者location下發被劫持域名和替換域名對應關係的配置進行域名替換。
2.如何替換呢?在loadUrl的時候就要根據拉取的配置是否需要替換域名,另外在WebView的shouldInterceptRequest回調函數中攔截頁面的子請求,拉取服務器配置判斷是否要替換域名,並且替換後的域名也要注入自簽名證書。
3.收到sslError後再次向服務器發送一個請求,校驗證書的合法性,校驗通過則忽略證書錯誤,校驗不通過則顯示證書錯誤的對話框。
4.更簡單直接的方案是前端同學去修改,直接更改所有請求方式通過前端與本地之前的橋走本地代發,這時候本地網絡請求的代碼肯定攜帶了自簽名的證書,這時候網頁內的請求也攜帶了自簽名的證書。

四,校驗自簽名證書的原理和關鍵代碼

1.需要攔截處理的位置。

//1.加載網頁時需要判斷是否替換url的域名
public void loadUrl(String url) {
    }

//2.WebViewClient中需要處理的函數
private WebViewClient webViewClient = new WebViewClient() {
      
      @Nullable
        @Override
        public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
            //交給webview自己處理時,需要替換攔截後的域名
            String interceptUrl = getInterceptUrl(url);
            return super.shouldInterceptRequest(view, interceptUrl);
        }

        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        @Nullable
        @Override
        public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
            //交給webview自己處理時,需要替換攔截後的域名
            String interceptUrl = getInterceptUrl(request.getUrl().toString());
            return super.shouldInterceptRequest(view, interceptUrl);
        }
        @Override
        public void onReceivedSslError(WebView view, final SslErrorHandler handler, SslError error) {
           //收到sslError後需要向服務器發一個請求再次校驗證書的合法性,校驗成功忽略證書錯誤,校驗失敗則取消
           //忽略證書錯誤
           handler.proceed();
           //取消
           handler.cancel();
        }
    };

2.使用Okhttp校驗自簽名證書的方式

	//獲取OkhttpClient,getCustomSSL()是獲取ssl證書的函數
	private static OkHttpClient getInterceptClient() {
        if (interceptClient == null) {
            try {
                OkHttpClient.Builder builder = getOkHttpClientBuilder();
                builder.sslSocketFactory(getCustomSSL()).followRedirects(false);
                interceptClient = builder.build();
            } catch (Throwable thr) {
                thr.printStackTrace();
            }
        }
        return interceptClient;
    }
    
	//獲取SSLSocketFactory
 	private static SSLSocketFactory getCustomSSL() throws IOException {
        if (customSSL != null) {
            return customSSL;
        }
        String caFilename = "xxxCA.der";
        InputStream caInput = new BufferedInputStream(RefereeService.getInstance().getContext().getAssets().open(caFilename));
        InputStream[] cas = {caInput};
        customSSL = getSslSocketFactory(cas, null, null);
        return customSSL;
    }
	//根據證書密碼等獲取SSLSocketFactory
 	 public static SSLSocketFactory getSslSocketFactory(InputStream[] certificates, InputStream bksFile, String password) {
        try {
            KeyManager[] keyManagers = prepareKeyManager(bksFile, password);
            TrustManager[] trustManagers = prepareTrustManager(certificates);
            SSLContext sslContext = SSLContext.getInstance("TLS");
            TrustManager trustManager;
            if (trustManagers != null) {
                trustManager = new MyTrustManager(chooseTrustManager(trustManagers));
            } else {
                trustManager = new UnSafeTrustManager();
            }
            sslContext.init(keyManagers, new TrustManager[]{trustManager}, new SecureRandom());
            return sslContext.getSocketFactory();
        } catch (NoSuchAlgorithmException e) {
            throw new AssertionError(e);
        } catch (KeyManagementException e) {
            throw new AssertionError(e);
        } catch (KeyStoreException e) {
            throw new AssertionError(e);
        }
    }
    
	//準備KeyManager
	private static KeyManager[] prepareKeyManager(InputStream bksFile, String password) {
        try {
            if (bksFile == null || password == null) return null;
            KeyStore clientKeyStore = KeyStore.getInstance("BKS");
            clientKeyStore.load(bksFile, password.toCharArray());
            KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
            keyManagerFactory.init(clientKeyStore, password.toCharArray());
            return keyManagerFactory.getKeyManagers();
        } catch (KeyStoreException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (UnrecoverableKeyException e) {
            e.printStackTrace();
        } catch (CertificateException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
 	 //準備證書信任的Manager
  	private static TrustManager[] prepareTrustManager(InputStream... certificates) {
        if (certificates == null || certificates.length <= 0) return null;
        try {
            CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            keyStore.load(null);
            int index = 0;
            for (InputStream certificate : certificates) {
                String certificateAlias = Integer.toString(index++);
                keyStore.setCertificateEntry(certificateAlias, certificateFactory.generateCertificate(certificate));
                try {
                    if (certificate != null) certificate.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            TrustManagerFactory trustManagerFactory;
            trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            trustManagerFactory.init(keyStore);
            return trustManagerFactory.getTrustManagers();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (CertificateException e) {
            e.printStackTrace();
        } catch (KeyStoreException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
	private static X509TrustManager chooseTrustManager(TrustManager[] trustManagers) {
        for (TrustManager trustManager : trustManagers) {
            if (trustManager instanceof X509TrustManager) {
                return (X509TrustManager) trustManager;
            }
        }
        return null;
    }

    private static class UnSafeTrustManager implements X509TrustManager {
        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        }

        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        }

        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return new X509Certificate[]{};
        }
    }

    private static class MyTrustManager implements X509TrustManager {
        private X509TrustManager defaultTrustManager;
        private X509TrustManager localTrustManager;

        public MyTrustManager(X509TrustManager localTrustManager) throws NoSuchAlgorithmException, KeyStoreException {
            TrustManagerFactory var4 = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            var4.init((KeyStore) null);
            defaultTrustManager = chooseTrustManager(var4.getTrustManagers());
            this.localTrustManager = localTrustManager;
        }

        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {

        }

        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            try {
                defaultTrustManager.checkServerTrusted(chain, authType);
            } catch (CertificateException ce) {
                localTrustManager.checkServerTrusted(chain, authType);
            }
        }

        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return new X509Certificate[0];
        }
    }

3.校驗自簽名證書安全的方案其實本質上就是SSL加密與解密的過程,要想深刻理解還需要看一下SSL的實現方式。

五,總結

1.WebView是一個很大的方向,目前也是一個很成熟的控件。其實前端跨平臺的方案還有weex,lua,flutter等等,萬變不離其宗,這些跨平臺方案目前能滿足大多數的性能要求,如果對性能要求更高的話還是要原生去實現,出現問題時可以使用google瀏覽器提供的google://inspect工具去調試。
2.跨平臺開發的另一個方向其實是NDK,也就是編譯C,C++代碼來達到數據的安全性和更高的性能要求,後續會寫一篇文章詳細介紹NDK編程相關的知識。

本人所學知識有限,如有描述不合理出請指正,如有問題也可以給本人發郵件,郵箱地址:[email protected]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章