原有项目中有部分界面是用webview展现的h5页面,一直以来都使用的http地址,但有些情况下,用户dns被劫持,页面上出现了一些广告的内容,或者页面就是白屏,总结起来还是因为使用http,页面内容被劫持修改,修改后的内容要么多出广告,要么被修改得加载不出来,因此项目中自然就需要加载https的地址。同时修改为https之后,出现问题的那些用户也能正常的显示出h5页面内容。
然而,在新版本上线后,有客户反馈oppo(8.1)、vivo等用户反馈app中页面白屏,单位的oppo和vivo验证都没有问题。最后小伙伴用模拟器复现了,同时也看到错误日志:
此处的日志是在WebChromeClient的重实现打出来的(代码已经修改过):
@Override
public void onReceivedSslError(WebView webView, SslErrorHandler sslErrorHandler, SslError sslError) {
GenseeLog.e(TAG,"onReceivedSslError sslErrorHandler = [" + sslErrorHandler + "], sslError = [" + sslError + "]");
_onReceivedError();
super.onReceivedSslError(webView, sslErrorHandler, sslError);
}
得知是在WebChromeClient的onReceivedSslError重实现中响应了相关的错误,在onReceivedSslError中调用sslErrorHandler.process();
就可以了:忽略证书问题。
public void onReceivedSslError(WebView webView, SslErrorHandler sslErrorHandler, SslError sslError) {
GenseeLog.e(TAG,"onReceivedSslError sslErrorHandler = [" + sslErrorHandler + "], sslError = [" + sslError.getPrimaryError() + "]");
_onReceivedError();
sslErrorHandler.proceed();
// super.onReceivedSslError(webView, sslErrorHandler, sslError);
}
此处注意:不要调用super.onReceivedSslError(webView, sslErrorHandler, sslError),super的实现是sslErrorHandler.cancel();
,终止访问。
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
handler.cancel();
}
注意:文本是通过忽略证书的问题处理的,但从实质性的安全来说并没起到作用。确认我们的证书是无误的。唯独几个oppo(系统8.1)和vivo用户有问题,目前没有得到真机的验证,推测还是系统证书下载问题。有知道的老铁请留言,感谢之。
正确建议做法是做证书校验:
webView.setWebViewClient(new WebViewClient() {
override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
String msg;
switch(error.getPrimaryError()) {
case: SslError.SSL_DATE_INVALID
msg = "证书日期无效"
break;
case: SslError.SSL_EXPIRED
msg = "证书已过期。"
break;
case: SslError.SSL_IDMISMATCH
msg = "主机名不匹配。"
break;
case: SslError.SSL_INVALID
msg = "发生一般错误"
break;
case: SslError.SSL_MAX_ERROR
msg = "不同SSL错误的数量。"
break;
case: SslError.SSL_NOTYETVALID
msg = "证书尚未生效。"
break;
case: SslError.SSL_UNTRUSTED
msg = "证书颁发机构不受信任。" // 自定义证书会执行到这个分支来
break;
default:
msg ="SSL证书错误,错误码:"+ error.getPrimaryError();
}
Log.i("SSL错误:" + msg)
if (error.getPrimaryError() == SslError.SSL_UNTRUSTED) {
// 证书颁发机构不受信任,则我们需要判断一下是否是我们自己的自定义证书,是的话就忽略这个错误
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
X509Certificate certificate = certificateFactory.generateCertificate(resources.openRawResource(R.raw.xxx)) ;
Field mX509CertificateFiled = SslCertificate.getClass().getDeclaredField("mX509Certificate");
mX509CertificateFiled .setAccessible( true);
X509Certificate mX509Certificate = mX509CertificateFiled.get(error.certificate());
val certificates = HandshakeCertificates.Builder()
.addTrustedCertificate(certificate) // 信任指定的自定义证书
.addPlatformTrustedCertificates() // 信任系统的预装证书,如果不信任系统证书的话,比如在访问https://m.baidu.com时将会出错
.build()
try {
certificates.trustManager.checkServerTrusted(new X509Certificate []{mX509Certificate}), "RSA")
Log.i("是我们的自定义证书")
handler.proceed()
} catch (e: java.lang.Exception) {
Log.e(e, "非法证书")
handle.cancel()
}
}
} else {
super.onReceivedSslError(view, handler, error)
}
}
});
在一个页面通过http请求的,但页面内有https的资源,后者页面是通过https访问的,但页面内有http的资源,这样的混合,一般后者都存在问题,当前前者也可能存在问题。
在webview加载页面之前,设置加载模式为MIXED_CONTENT_ALWAYS_ALLOW
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
webView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}
在Android5.0之前,系统默认是采用的MIXED_CONTENT_ALWAYS_ALLOW模式,即总是允许WebView同时加载Https和Http;而从Android5.0开始,默认用MIXED_CONTENT_NEVER_ALLOW模式,即总是不允许WebView同时加载Https和Http。
如果都是自家的网页,那这个问题还是从源头上进行处理,页面不要使用混合的方式。
官网给出的建议是,为了安全考虑,使用 MIXED_CONTENT_NEVER_ALLOW模式,但是在实际引用中,当我们的服务器已经升级到Https,但是一些页面的资源是第三方的,我们不一定能要求第三方也都升级到Https,所以我们只能根据系统版本,用代码去设置加载模式为MIXED_CONTENT_ALWAYS_ALLOW。
从Android5.0开始,当一个安全的站点(https)去加载一个非安全的站点(http)时,需要配置Webview加载内容的混合模式,一共有如下三种模式:
如果需要配置证书的情况下,
在res目录下创建一个xml文件目录,再创建一个network_security_config.xml(名字可随意),然后就是在这个文件中写入一些关于https的配置,接着把network_security_config配置到Anroidmanifest的Application节点属性中,如下:
<application
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config">
application>
我们的xxx.crt的证书文件按照规范,需要放到res/raw/目录下面。接着配置到network_security_config文件中。配置证书,有两种方式,如下:
<network-security-config>
<base-config>
<trust-anchors>
<certificates src="@raw/xxx" />
trust-anchors>
base-config>
network-security-config>
<network-security-config>
<domain-config>
<domain includeSubdomains="true">gensee.comdomain>
<trust-anchors>
<certificates src="@raw/xxx"/>
trust-anchors>
domain-config>
network-security-config>
<network-security-config>
<base-config>
<trust-anchors>
<certificates src="@raw/custom_ca" />
<certificates src="system" />
<certificates src="user" />
trust-anchors>
base-config>
network-security-config>
val builder = OkHttpClient.Builder()
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
val certificateFactory: CertificateFactory = CertificateFactory.getInstance("X.509")
val certificate = certificateFactory.generateCertificate(resources.openRawResource(R.raw.xxx)) as X509Certificate
val certificates = HandshakeCertificates.Builder()
.addTrustedCertificate(certificate) // 信任指定的自定义证书
.addPlatformTrustedCertificates() // 信任系统的预装证书,如果不信任系统证书的话,比如在访问https://m.baidu.com时将会出错
.build()
builder.sslSocketFactory(certificates.sslSocketFactory(), certificates.trustManager)
}
val okHttpClient = builder.build()
// Load CAs from an InputStream
// (could be from a resource or ByteArrayInputStream or ...)
val cf: CertificateFactory = CertificateFactory.getInstance("X.509")
// From https://www.washington.edu/itconnect/security/ca/load-der.crt
val caInput: InputStream = BufferedInputStream(FileInputStream("load-der.crt"))
val ca: X509Certificate = caInput.use {
cf.generateCertificate(it) as X509Certificate
}
System.out.println("ca=" + ca.subjectDN)
// Create a KeyStore containing our trusted CAs
val keyStoreType = KeyStore.getDefaultType()
val keyStore = KeyStore.getInstance(keyStoreType).apply {
load(null, null)
setCertificateEntry("ca", ca)
}
// Create a TrustManager that trusts the CAs inputStream our KeyStore
val tmfAlgorithm: String = TrustManagerFactory.getDefaultAlgorithm()
val tmf: TrustManagerFactory = TrustManagerFactory.getInstance(tmfAlgorithm).apply {
init(keyStore)
}
// Create an SSLContext that uses our TrustManager
val context: SSLContext = SSLContext.getInstance("TLS").apply {
init(null, tmf.trustManagers, null)
}
// Tell the URLConnection to use a SocketFactory from our SSLContext
val url = URL("https://certs.cac.washington.edu/CAtest/")
val urlConnection = url.openConnection() as HttpsURLConnection
urlConnection.sslSocketFactory = context.socketFactory
val inputStream: InputStream = urlConnection.inputStream
copyInputStreamToOutputStream(inputStream, System.out)
/** 获取一个SSLSocketFactory */
val sSLSocketFactory: SSLSocketFactory
get() = try {
val sslContext = SSLContext.getInstance("SSL")
sslContext.init(null, arrayOf(trustManager), SecureRandom())
sslContext.socketFactory
} catch (e: Exception) {
throw RuntimeException(e)
}
/** 获取一个忽略证书的X509TrustManager */
val trustManager: X509TrustManager
get() = object : X509TrustManager {
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) { }
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) { }
override fun getAcceptedIssuers(): Array<X509Certificate> { return arrayOf() }
}
将这两个对象设置给okhttp或HttpsURLConnection即可完成证书忽略。
https://blog.csdn.net/android_cai_niao/article/details/108065766
https://blog.csdn.net/luofen521/article/details/51783914