一,背景介紹
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]