前言:只要涉及到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)艾特我,感謝!