前言:只要涉及到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就是用不了了。

此文只供参考,如有哪位大神有完美的解决方案,请艾特我,感谢!

原文地址:https://www.jianshu.com/p/56e2b0bf9ab2

发表评论

邮箱地址不会被公开。 必填项已用*标注