WebView加载https链接
前言:只要涉及到https,大家都会第一时间想到证书验证。当然,这是没问题的。如果有要求,这个证书验证是必须的。一般情况下,都是需要证书的(有证书毕竟安全些吗)
正常的解决办法:
重写类WebViewClient
第一种方法:
webView.setWebViewClient(new WebViewClient ()
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
super.onReceivedSslError(view, handler, error);
}
});
说明:Android WebView组件加载网页发生证书认证错误时,会调用WebViewClient类的onReceivedSslError方法,在这个方法里,我们可以点击源码看到SslErrorHandler中有两个主要的方法可以调用
【1】cancel( )
停止加载问题页面
【2】proceed( )
忽略SSL证书错误,继续加载页面
如果我们不考虑证书安全,则可以直接这样写
webView.setWebViewClient(new WebViewClient ()
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
handler.proceed();
}
});
当然了,既然用到了https,我们大部分时候都是为了网络安全问题
所以,可以定义一个类来验证证书安全,代码如下:
/**
* 时间 :2018/1/11 10:17
* 作者 :陈奇
* 作用 :证书验证的方法
*/
public class CertifiUtils {
// 验证证书
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("证书验证失败", e.getMessage());
handler.cancel();
}
@Override
public void onResponse(Call call, Response response) throws IOException {
BNLog.e("证书验证成功", response.body().string());
handler.proceed();
}
});
}
private static OkHttpClient.Builder setCertificates(OkHttpClient.Builder client) {
try {
//Xutils.getSSLContext:获取证书SSLSocketFactory(这个网络上有很多代码,并且我之前的文章里也有写出来,在这里就不过多的描述了)
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() );
}
});
但是,还有种情况是这样的,当webview加载url时,它并不调用onReceivedSslError方法,而是在onReceivedError方法里返回net::ERR_SSL_PROTOCOL_ERROR。原因呢,当时我调用是我们的链接,而这个链接则请求的是第三方请求,但是这个第三方请求必须要验证证书,这个时候如果没有验证证书,第三方请求就会返回一个错误给我们的链接,然后在返回到客户端。也就是说第三方直接否定了这次请求,这个请求呢,就不是我们的链接能控制的了。所以这个时候是调用的是onReceivedError,而不调用onReceivedSslError。只是,最根本的原理是,webview加载html资源,默认支持的是http请求而不是https请求。所以我们是不是只要把http请求替换成带验证证书的https请求就好了?当然,通过证明,这是可行的。
在WebViewClient 类中就提供了拦截的方法shouldInterceptRequest
shouldInterceptRequest有两种重载。
从API 11开始引入,API 21弃用
public WebResourceResponse shouldInterceptRequest (WebView view, String url)
从API 21开始引入
public WebResourceResponse shouldInterceptRequest (WebView view, WebResourceRequest request)
综上所述,代码如下:
/**
* 时间 :2018/1/19 18:24
* 作者 :陈奇
* 作用 :自定义webViewClient,拦截http(s)请求,并对相应的加载证书验证
*/
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;
// 设置一个url链接
URL url = new URL(uri.toString());
// 如果是http请求,这里如果加载的是http请求,则也要建立HttpURLConnection而不是默认加载。
if (uri.toString().startsWith(APPConstants.CONFIGURATION_HTTP)) {
HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
is = httpURLConnection.getInputStream();
contentType = httpURLConnection.getContentType();
encoding = httpURLConnection.getContentEncoding();
} else { // 如果是https请求
HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
urlConnection.setSSLSocketFactory(sslContext.getSocketFactory());
is = urlConnection.getInputStream();
contentType = urlConnection.getContentType();
encoding = urlConnection.getContentEncoding();
}
// 如果信息头中的资源类型不为null,则继续
if (contentType != null) {
String mimeType = contentType;
// 解析出mimeType
if (contentType.contains(";")) {
mimeType = contentType.split(";")[0].trim();
}
Log.d("SSL_PINNING_WEBVIEWS", "Mime: " + mimeType);
// 返回设置重新设置过的请求
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());
}
// 返回一个空的请求
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、设置getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
2、或者使用上面的SslPinningWebViewClient
使用时是需要调用就可以了
webView.setWebViewClient(new SslPinningWebViewClient() {})
缺点:这种重写,虽然可以加载出https和http,但是如果这个h5页面中含有post带参请求,在拦截的时候是拦截不出来参数的。这就导致了h5中有的地方点击是有问题!!!
第二种方案:
/**
* 时间 :2018/1/25 14:48
* 作者 :陈奇
* 作用 :自定义webViewClient,加载https请求
*/
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";//客户端要给服务器端认证的证书
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();
}
}
缺点:只支持API21及以上的。在网上也有一些说是可以导入安卓4.2的源码架包,然后作为系统架包依赖进去,可是这样,4.2系统以上的新的API就是用不了了。
此文只供参考,如有哪位大神有完美的解决方案,请艾特我,感谢!