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