精华内容
下载资源
问答
  • 仔细检查了一下也没问题啊,路径也正确,仔细一看原来是图片路径是https的,正常用到的应该都是http的,https要比http更安全。 mWebView = (WebView) findViewById(R.id.mwebview); WebSettings webSettings = ...

    用webview加载网页不知道为什么图片居然不显示?仔细检查了一下也没问题啊,路径也正确,仔细一看原来是图片路径是https的,正常用到的应该都是http的,https要比http更安全。

    mWebView = (WebView) findViewById(R.id.mwebview);
    WebSettings webSettings = mWebView.getSettings();
    webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);

    
    
    
    但是,直接加这句话是不可以的,因为这句话是API19以上才可以使用,基于版本的
    

    兼容性我们需要加入一个判断,这样就解决了版本的问题。

    if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.LOLLIPOP) 
    webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);

    
    
    
    



    展开全文
  • HTTPS in webview production

    2020-12-06 07:11:39
    <p>I am having issue loading https based pages in Webview in production app. Is there a way around that. I have enabled cordova whitelist <p>` <pre><code><access origin="*" /> &...
  • Android HttpsWebView

    2020-08-17 22:59:59
    前言 今天公司说要把项目的网络请求改成...于是我又得花一翻功夫去修改代码以便可以访问https的请求,最开始是直接搜索的OkHttp信认证书、WebView信任证书,后来发现谷歌有一个更简单的设置信任证书的办法,那就是在xm

    前言

    今天公司说要把项目的网络请求改成https,如果证书是那种权威机构颁发的,则代码不需要做做任何修改,只需要网址改成https即可,不巧的是,我们公司的项目作用于内网,使用权威机构颁发的证书没什么用,因为要进行认证肯定要连外网才能认证得到,我们是内网,而且权威证书是要花钱买的,于是打算用一个自定义的证书(即自己生产的证书)。于是我又得花一翻功夫去修改代码以便可以访问https的请求,最开始是直接搜索的OkHttp信认证书、WebView信任证书,后来发现谷歌有一个更简单的设置信任证书的办法,那就是在xml中直接配置即可,不需要修改任何的Java代码,哎,所以有说,有时间还是要多逛逛Android官方文档,网上的答案很多都不是最好的,而官方上的东西则一般是最新的,最推荐使用的。

    官网在此。
    关于Https的攻击,还有:https://books.nowsecure.com/secure-mobile-development/en/sensitive-data/fully-validate-ssl-tls.htmlhttps://owasp.org/www-community/controls/Certificate_and_Public_Key_Pinning#Android
    官方中信任自定义证书的示例:https://developer.android.com/training/articles/security-ssl.html#UnknownCa

    一、信任指定的证书

    在res目录下创建一个xml文件,然后创建一个network_security_config.xml(官方用的是这个名字,用其他名字应该也没问题),然后就是在这个文件中写入一些关于https的配置,这跟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文件中配置我们的网络信任这个证书了,有两种方式,如下:

    方式一:

    <?xml version="1.0" encoding="utf-8"?>
    <network-security-config>
        <base-config>
            <trust-anchors>
                <certificates src="@raw/xxx" />
            </trust-anchors>
        </base-config>
    </network-security-config>
    

    方式二:

    <?xml version="1.0" encoding="utf-8"?>
    <network-security-config>
        <domain-config>
            <domain includeSubdomains="true">baidu.com</domain>
            <trust-anchors>
                <certificates src="@raw/xxx"/>
            </trust-anchors>
        </domain-config>
    </network-security-config>
    

    注:我们公司使用的是内网,又没有dns服务器,所以访问api接口都是直接使用ip的,所以上面的baidu.com的地方可以直接修改成ip地址,那个证书里面是要设置一个域名的,同时还要设置对应的ip,对应的ip可以设置多个的,如果证书里面没有指定的ip,则这个证书是无效的,使用时会报错。

    这两种方式的区别,方式二使用domain-config限制了baidu.com或它的子域名都信任我们指定的xxx证书,而方式一使用base-config则表示应用的访问的所有域名的资源都信任我们指定的xxx证书。要按安全性来说的话首先方式二,就为它的限制更高一些。

    用于配置我们要信任什么证书,直接使用@raw/xxx为配置信任自定义的证书,如果要指定信任预安装的证书,需要另外指定,预安装的证书有系统和用户两种类型,如下:

    <?xml version="1.0" encoding="utf-8"?>
    <network-security-config>
        <base-config>
            <trust-anchors>
                <certificates src="@raw/dazhoucn" /> <!--信任自定义的CA证书-->
                <certificates src="system" /> <!--信任系统预装的CA证书-->
                <certificates src="user" />   <!--信任用户安装的CA证书-->
            </trust-anchors>
        </base-config>
    </network-security-config>
    

    可以看到,我们可以指定信任多个证书,或者某一类型的证书(如预装的系统证书),这是使用方式一的好处,而使用方式二,一个domain-config中或以指定多个domain,但是这一个或多个domain只能指定信任一个指定的证书。如果不同的网站信任不同的证书,则可以配置多个domain-config标签来实现。

    在Android6.0及以下版本默认是会信任用户安装的CA证书的,7.0及更高版本默认就只信任预装的系统CA证书了,所以我们最好不要配置信任用户安装的CA证书,因为有可能黑客往手机装了一个证书进去呢!

    假如我们希望配置我们指定的网站只能使用https,而其他网站可以使用明文http,而且也信任系统证书,配置如下:

    <?xml version="1.0" encoding="utf-8"?>
    <network-security-config xmlns:tools="http://schemas.android.com/tools">
        <base-config cleartextTrafficPermitted="true"
            tools:ignore="InsecureBaseConfiguration">
            <!--base-config中设置了允许明文传输-->
            <trust-anchors>
                <certificates src="system"/> <!--信任系统证书-->
                <certificates src="@raw/xxx"/>
            </trust-anchors>
        </base-config>
        <domain-config cleartextTrafficPermitted="false">
            <!--domain-config中限制了xxx.com不能明文传输-->
            <domain includeSubdomains="true">xxx.com</domain>
            <trust-anchors>
                <certificates src="@raw/xxx"/>
            </trust-anchors>
        </domain-config>
    </network-security-config>
    

    在Android10中测试,如果不指定信任系统证书的话,WebView加载https网站是没问题的,但是OkHttp请求Https时就会报异常。

    二、WebView加载网站出不来的问题

    配置好Https之后当然要测试一下了,我就随便拿了个百度(https://m.baidu.com)来试,OkHttp请求是没问题的,但是WebView加载出不了,后来发现百度里面用到了JS,所以需要允许JS,如下:

    webView.settings.javaScriptEnabled = true
    

    系统会提示说不安全,因为网站通过js就能调用你的android代码,如果你确认你的网站没用到JS的话就不要打开这个开关,如果用到了,就添加一个注解忽略它就行了。

    后来就使用我们公司的网站了,发现也出不来,后来发现公司网站用到了dom存储,所以还需要打开这个开关:

    webView.settings.domStorageEnabled = true
    

    xml配置自定义证书对WebView也是生效的,但是我们上面也说了,xml配置的方式只对Android7.0或更高版本才有用,那在低版本中如何让WebView信任自定义证书呢?网上的答案是直接忽略证书,如下:

    webView.webViewClient = object: WebViewClient() {
                override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError) {
    	handler?.proceed()
    }
    

    这个onReceivedSslError函数默认是调用handler?.cancel()来处理SSL错误的,调用cancel即表示不与服务器进行通信,调用proceed即表示要与服务器通信(虽然证书有问题)。

    这样的做法是不安全的,而且这样的代码也无法把app上传到谷歌市场,因为必须要有对应的cancel调用,说白了就是要我们自己去验证证书的合法性,合法就调用proceed,否则调用cancel,代码如下(用到了OkHttp的相关类):

    webView.webViewClient = object: WebViewClient() {
            override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError) {
                    val message =  when (error.primaryError) {
                            SslError.SSL_DATE_INVALID -> "证书日期无效"
                            SslError.SSL_EXPIRED -> "证书已过期。"
                            SslError.SSL_IDMISMATCH -> "主机名不匹配。"
                            SslError.SSL_INVALID -> "发生一般错误"
                            SslError.SSL_MAX_ERROR -> "不同SSL错误的数量。"
                            SslError.SSL_NOTYETVALID -> "证书尚未生效。"
                            SslError.SSL_UNTRUSTED -> "证书颁发机构不受信任。" // 自定义证书会执行到这个分支来
                            else -> "SSL证书错误,错误码:${error.primaryError}"
                    }
                    Timber.i("SSL错误:$message")
    
                    if (error.primaryError == SslError.SSL_UNTRUSTED) {
                        // 证书颁发机构不受信任,则我们需要判断一下是否是我们自己的自定义证书,是的话就忽略这个错误
    
                        val certificateFactory: CertificateFactory = CertificateFactory.getInstance("X.509")
                        val certificate = certificateFactory.generateCertificate(resources.openRawResource(R.raw.xxx)) as X509Certificate
                        val mX509CertificateFiled = SslCertificate::class.java.getDeclaredField("mX509Certificate").apply { isAccessible = true }
                        val mX509Certificate = mX509CertificateFiled.get(error.certificate) as X509Certificate
                        val certificates = HandshakeCertificates.Builder()
                            .addTrustedCertificate(certificate) // 信任指定的自定义证书
                            .addPlatformTrustedCertificates()  // 信任系统的预装证书,如果不信任系统证书的话,比如在访问https://m.baidu.com时将会出错
                            .build()
    
                        try {
                            certificates.trustManager.checkServerTrusted(arrayOf(mX509Certificate), "RSA")
                            Timber.i("是我们的自定义证书")
                            handler?.proceed()
                        } catch (e: java.lang.Exception) {
                            Timber.e(e, "非法证书")
                            handler?.cancel()
                        }
                    }
                } else {
                    super.onReceivedSslError(view, handler, error)
                }
            }
    }
    

    上面代码做了版本判断,因为Android7.0或以上版本直接使用xml中的配置。

    主要原理就是把手机本地的自定义证书实例化到代码中,并封装到X509TrustManager对象中,此对方就能用于判断服务器的证书和我们的证书是否是在一个合法的证书链里面的,通过调用error.certificate.x509Certificate得到服务器上的证书,通过trustManager.checkServerTrusted(arrayOf(mX509Certificate), “RSA”)来检查服务器的证书和我们的证书是否是在一个合法的链上的,如果合法就正常通过调用,不合法就抛出异常。

    开始我是直接比较本地的证书和服务器的证书是否一样来实现的,后来服务器改了,服务器先生成一个证书,再通过这个证书又签名出另一个证书,证书还能再签名出别的证书,这就是一条链,现在手机端和服务器端上的证书是不一样的了,但是因为他们是在同一个链的,所以也能认证通过,所以这种情况下不能使用比较是否是同一个证书的做法,而是比较是否是同一个链。
    比较是否是同一个证书的代码也很简单,如下:

    val isSameCertifiate = certificate == error.certificate.x509Certificate
    

    这里用的是kotlin语言,实现是调用equals方法比较的,equals方法中的实现是把证书读取为编码后的字节数据,然后比较两个数组是否一样。

    当服务器端和客户端一个是根证书,一个是由根证书颁发的子证书时,还可以用另一种方法验证,先说明一下证书生成的情况:
    一、根证书

    • 根公钥
    • 根私钥
    • 根证书(装有根公钥,使用根私钥签名)

    二、中间证书1

    • 中间公钥1
    • 中间私钥1
    • 中间证书1 (装有中间公钥1,并用根私钥签名)

    三、中间证书2

    • 中间公钥2
    • 中间私钥2
    • 中间证书2 (装有中间公钥2,并用根私钥签名)

    我们知道rsa签名的规则为:私钥签名,对应的公钥验证签名。中间证书1和中间证书2都是用根证书的私钥签名的,所以可使用根证书中的公钥进行验证中间证书1和2中的签名。

    反过来就不行了,根证书中的签名无法使用中间证书中的公钥验证,因为根证书的签名不是用中间证书的私钥签名的。

    中间证书1的签名也无法用中间证书2的公钥进行验证,因为中间证书1的签名不是使用中间证书2的私钥签名的。

    OK,了解了这个原理之后,我们就可以实现在WebView中,使用公钥来验证服务器的证书是否是我们公司的证书。在我们公司的项目中,也存在上面结构的一些证书,根证书放在手机端,中间证书放在了服务器端,所以可以使用根证书的公钥来验证中间证书的签名,如果能验证通过,说明服务器上的证书是可信(不是别人公司的),伪代码如下:
    中间证书.验证签名(根证书.公钥),翻译成代码如下:

    middleCert.verifySign(rootCert.publicKey)
    

    真实代码如下:

    val certificateFactory: CertificateFactory = CertificateFactory.getInstance("X.509")
    val rootCert = certificateFactory.generateCertificate(resources.openRawResource(R.raw.rootCert))
    val middleCert = error.certificate.x509Certificate
    try {
        middleCert.verify(rootCert.publicKey)
        Timber.i("验证通过")
    } catch (e: java.lang.Exception) {
        Timber.e(e,"验证失败")
    }
    

    中间证书也可能会颁发子证书,但是只能使用父证书的公钥来验证子证书的签名,反过来就不行。

    三、Android6.0及更低版本的CA证书信任处理

    测试的时候发现上面的设置方法对于Android6.0及更低版本无效,通过了解才知道了原因:

    在Android 6.0的时候,在清单文件的application节点中新增了一个属性android:usesCleartextTraffic,含义为“使用明文通信”,设置为true则为允许使用http(明文)请求,设置为false则不允许使用http请求,只能使用https(加密)请求。

    在Android7.0的时候,新增了通过network_security_config.xml的方式来配置https请求。

    所以,xml配置https是在7.0的时候才出来的,用到更低的版本上肯定是不生效的,谷歌官网上只是说了在Android6.0的版本时它的默认设置是怎样的,并没有说我们在xml中的设置可以在Android6.0中起作用。

    谷哥说的各种版本的https默认配置如下:

    Android 9(API 级别 28)及更高版本为目标平台的应用的默认配置如下所示:

        <base-config cleartextTrafficPermitted="false">
            <trust-anchors>
                <certificates src="system" />
            </trust-anchors>
        </base-config>
    

    可以看到,当你在gradle中把目标版本设置为false时,默认的https配置是不允许使用明文通信的(http通信),而且默认信任系统类型的预装CA证书。所以,当我们他创建一个新项目的时候,默认目标版本都29或30或更高,我们声明了网络访问权限,确发现访问http时访问不了,就是因为默认不允许使用http了,如果你坚持想要使用http(明文通信),则可以把cleartextTrafficPermitted设置为true即可。

    Android 7.0(API 级别 24)到 Android 8.1(API 级别 27)为目标平台的应用的默认配置如下所示:

        <base-config cleartextTrafficPermitted="true">
            <trust-anchors>
                <certificates src="system" />
            </trust-anchors>
        </base-config>
        
    

    以 Android 6.0(API 级别 23)及更低版本为目标平台的应用的默认配置如下所示:

        <base-config cleartextTrafficPermitted="true">
            <trust-anchors>
                <certificates src="system" />
                <certificates src="user" />
            </trust-anchors>
        </base-config>
        
    

    这只是说明Android6.0中关于https的默认行为配置是这样的,并不等于你可以在xml中修改一下就能修改到这些配置,因为在Android6.0的时候还没有使用xml进行配置的方式。

    那Android6.0及更低版本应该如何处理呢?看国外文章有说可以使用:https://github.com/datatheorem/TrustKit-Android,该库可使用和高版本的方式一样配置,并兼容低版本,但是我使用时不行,不知道是不是因为我使用的是ip访问,而不是域名方法,它的初始化代码中使用到了域名。
    还有另一个库也可以:https://github.com/commonsguy/cwac-netsecurity/blob/master/README-original.markdown

    一个可以支持证书的自定义WebView:https://github.com/yonekawa/webview-with-client-certificate

    一个很多人讨论的关于WebView中使用自定义证书的事:https://issuetracker.google.com/issues/36917164

    实现双向认证:https://blog.csdn.net/kpioneer123/article/details/51491739

    https的版本支持:https://blog.csdn.net/ceko_wu/article/details/50954678

    最后,还是通过设置OkHttp来完成低版本的Https认证,如下:

    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()
    

    这里也做了版本判断,因为Android7.0或以上版本直接使用xml中的配置即可。

    谷歌官方实现链接:https://developer.android.google.cn/training/articles/security-ssl#UnknownCa ,因为没有用到OkHttp,所以会麻烦一些。代码如下:

        // 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)
        
    

    后来发现公司创建的证书有问题,公司创建了根证书,又创建了子证书,使用时在android5.0、6.0、8.0、9.0、10.0都没问题,就是在Android7.0出了问题,刚开始以为是代码哪里不对,于是我按着Google官方Demo也创建了一个证书,也自己搭了服务器,证书用在Android7.0是没问题的,证明是证书的问题,但是证书是哪里设置有问题了导致不兼容Android7.0,服务器人员也不知道(服务器人员创建的证书),所以解决方案就是在Android7.0的时候不验证证书,直接忽略,实现代码如下:

    /** 获取一个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中即可实现忽略证书:

    val builder = OkHttpClient.Builder()
    builder.sslSocketFactory(sSLSocketFactory, trustManager)
    val okHttpClient = builder.build()
    

    细节

    1. 加载网络图片时使用了Glide框架,也需要证书配置,可看我的这篇文章
    2. 基于Android版本的不同表现,代码中根据版本进行相应的设置,比如对于OkHttp,在Android版本为7.x时使用忽略证书的设置,在低于Android7的版本中使用OkHttp配置自定义证书,在Android8.0以及更高的版本中,不设置Okhttp的证书设置,因为这些高版会直接使用xml中的配置即可,这也是最好的,什么OkHttp请求,什么WebView,什么Glide,在高版本统统不需要设置,只要配置好了xml即可。
    展开全文
  • Https-webview配置

    2017-02-08 09:33:40
    其实如果我们的证书是合法证书不是自己生成的也就不需要配置下面的内容 ... public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { handler.proceed();//通过主机验证 }

    其实如果我们的证书是合法证书不是自己生成的也就不需要配置下面的内容
    配置如下:

     @Override
     public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
              handler.proceed();//通过主机验证
      }
    展开全文
  • HTTPS简介 HTTPS(Hyper Text Transfer Protocol Secure),是一种基于SSL/TLS的HTTP,所有的HTTP数据都是在SSL/TLS协议封装之上进行传输的。HTTPS协议是在HTTP协议的基础上,添加了SSL/TLS握手以及数据加密传输,也...

    HTTPS简介

    HTTPS(Hyper Text Transfer Protocol Secure),是一种基于SSL/TLS的HTTP,所有的HTTP数据都是在SSL/TLS协议封装之上进行传输的。HTTPS协议是在HTTP协议的基础上,添加了SSL/TLS握手以及数据加密传输,也属于应用层协议。所以,研究HTTPS协议原理,最终其实就是研究SSL/TLS协议。

    SSL/TLS协议

    不使用SSL/TLS的HTTP通信,就是不加密的通信,所有的信息明文传播,带来了三大风险:

    窃听风险:第三方可以获知通信内容。

    篡改风险:第三方可以修改通知内容。

    冒充风险:第三方可以冒充他人身份参与通信。

    SSL/TLS协议是为了解决这三大风险而设计的,希望达到:

    所有信息都是加密传输,第三方无法窃听。

    具有校验机制,一旦被篡改,通信双方都会立刻发现。

    配备身份证书,防止身份被冒充。

    HTTPS基本运行过程

    SSL/TLS协议的基本思路是采用公钥加密法,也就是说,客户端先向服务器端索要公钥,然后用公钥加密信息,服务器收到密文后,用自己的私钥解密,这就是非对称加密。但是这里需要了解两个问题的解决方案。

    如何保证公钥不被篡改?
    解决方法:将公钥放在数字证书中。只要证书是可信的,公钥就是可信的。

    公钥加密计算量太大,如何减少耗用的时间?
    解决方法:每一次对话(session),客户端和服务器端都生成一个“对话密钥”(session key),用它来加密信息。由于“对话密钥”是对称加密,所以运算速度非常快,而服务器公钥只用于加密“对话密钥”本身,这样就减少了加密运算的消耗时间。

    因此,SSL/TLS协议的基本过程是这样的:

    客户端向服务器端索要并验证公钥。

    双方协商生成“对话密钥”。

    双方采用“对话密钥”进行加密通信。

    上面过程的前两步,又称为“握手阶段”。


    客户端发起HTTPS请求

    服务端的配置
    采用HTTPS协议的服务器必须要有一套数字证书,可以是自己制作或者CA证书。区别就是自己颁发的证书需要客户端验证通过,才可以继续访问,而使用CA证书则不会弹出提示页面。这套证书其实就是一对公钥和私钥。公钥给别人加密使用,私钥给自己解密使用。

    传送证书
    这个证书其实就是公钥,只是包含了很多信息,如证书的颁发机构,过期时间等。

    客户端解析证书
    这部分工作是有客户端的TLS来完成的,首先会验证公钥是否有效,比如颁发机构,过期时间等,如果发现异常,则会弹出一个警告框,提示证书存在问题。如果证书没有问题,那么就生成一个随即值,然后用证书对该随机值进行加密。

    传送加密信息
    这部分传送的是用证书加密后的随机值,目的就是让服务端得到这个随机值,以后客户端和服务端的通信就可以通过这个随机值来进行加密解密了。

    服务端解密信息
    服务端用私钥解密后,得到了客户端传过来的随机值(私钥),然后把内容通过该值进行对称加密。所谓对称加密就是,将信息和私钥通过某种算法混合在一起,这样除非知道私钥,不然无法获取内容,而正好客户端和服务端都知道这个私钥,所以只要加密算法够彪悍,私钥够复杂,数据就够安全。

    传输加密后的信息
    这部分信息是服务端用私钥加密后的信息,可以在客户端被还原。

    客户端解密信息
    客户端用之前生成的私钥解密服务端传过来的信息,于是获取了解密后的内容。

    整个握手过程第三方即使监听到了数据,也束手无策。

    HTTP和HTTPS的区别

    https协议需要到CA申请证书或自制证书。
    http的信息是明文传输,https则是具有安全性的ssl加密。
    http是直接与TCP进行数据传输,而https是经过一层SSL(OSI表示层),用的端口也不一样,前者是80(需要国内备案),后者是443。
    http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。

    Android实现HTTPS通信

    CA认证的数字证书网站

    以百度的https网址(https://m.baidu.com/)为例,示例源码如下:

    public void startHttpsConnection() {
        HttpsURLConnection httpsURLConnection = null;
        BufferedReader reader = null;
        try {
            URL url = new URL("https://m.baidu.com/");
            httpsURLConnection = (HttpsURLConnection) url.openConnection();
            httpsURLConnection.setConnectTimeout(5000);
            httpsURLConnection.setDoInput(true);
            httpsURLConnection.setUseCaches(false);
            httpsURLConnection.connect();
    
            reader = new BufferedReader(new InputStreamReader(httpsURLConnection.getInputStream()));
            StringBuilder sBuilder = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                sBuilder.append(line);
            }
            Log.e("TAG", "Wiki content=" + sBuilder.toString());
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (httpsURLConnection != null) {
                httpsURLConnection.disconnect();
            }
    
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    由于百度是有CA授权的数字证书,所以这里我们就是简单的使用HttpsUrlConnection对其进行访问,就实现了HTTPS通信。

    自签名的数字证书网站

    由于CA认证是需要收费的,所以有些网站为了节约成本,采用自签名的数字证书,比如,注明的购票网站12306目前依然是这么干的。如果我们用上述代码访问自签名的网站会有什么问题呢? 访问自签名证书的网站,Android直接会throw SSLHandshakeException,原因就是12306的数字证书不被Android系统的信任。想解决这个问题,有如下几种方法。

    让HttpsURLConnection信任所有的CA证书

    1. 实现X509TrustManager接口,在接口实现中跳过客户端和服务器端认证。

    public class TrustAllCertsManager implements X509TrustManager {
        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType)
                throws CertificateException {
            // Do nothing -> accept any certificates
        }
    
        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType)
                throws CertificateException {
            // Do nothing -> accept any certificates
        }
    
        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return new X509Certificate[0];
        }
    }

    2. 实现HostnameVerifier接口,不进行url和服务器主机名的验证。

    public class VerifyEverythingHostnameVerifier implements HostnameVerifier {
        @Override
        public boolean verify(String hostname, SSLSession session) {
            return true;
        }
    }

    3. 基于上面实现的TrustAllCertsManager修改HttpsURLConnection类的默认SSL socket factory。
    TrustManager[] trustManager = new TrustManager[] {new TrustEverythingTrustManager()};
    SSLContext sslContext = null;
    try {
        sslContext = SSLContext.getInstance("SSL");
        sslContext.init(null, trustManager, new java.security.SecureRandom());
    } catch (NoSuchAlgorithmException e) {
        // do nothing
    }catch (KeyManagementException e) {
        // do nothing
    }
    HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
    4. 实例化HttpsUrlConnection,并设置HostnameVerifier为上面实现的VerifyEverythingHostnameVerifier

    httpsURLConnection = (HttpsURLConnection) url.openConnection();
    httpsURLConnection.setHostnameVerifier(new VerifyEverythingHostnameVerifier());
    但是以上方法存在严重的安全漏洞,因为默认相信一切证书,其实是忽略了证书验证。比如注明的中间人攻击:虽然上述方案使用了HTTPS,客户端和服务器端的通信内容得到了加密,嗅探程序无法得到传输的内容,但是无法抵挡“中间人攻击”。例如,在内网配置一个DNS,把目标服务器域名解析到本地的一个地址,然后在这个地址上使用一个中间服务器作为代理,它使用一个假的证书与客户端通讯,然后再由这个代理服务器作为客户端连接到实际的服务器,用真的证书与服务器通讯。这样所有的通讯内容都会经过这个代理,而客户端不会感知,这是由于客户端不校验服务器公钥证书导致的。

    让HttpsURLConnection信任指定的CA证书

    为了防止上面方案可能导致的“中间人攻击”,我们可以事先下载服务器端公钥证书,然后将公钥证书编译到Android应用中,由应用自己来验证证书。也就是我们来教会HttpsUrlConnection来认识特定的自签名网站。还是以12306网站为例。

    1. 下载12306的服务器公钥证书

    12306公钥证书下载地址:12306根证书下载地址

    2. 将下载的证书放到应用的assets目录下.

    app->src->main->assets->srca.cer
    (ps:使用Android Studio的同学需要特别注意默认asserts目录的位置)。

    3. 构造特定的TrustManager[]数组.

    private TrustManager[] createTrustManager() {
        BufferedInputStream cerInputStream = null;
        try {
            // 获取客户端存放的服务器公钥证书
            cerInputStream = new BufferedInputStream(getAssets().open("srca.cer"));
            // 根据公钥证书生成Certificate对象
            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            Certificate ca = cf.generateCertificate(cerInputStream);
            Log.e("TAG", "ca=" + ((X509Certificate) ca).getSubjectDN());
    
            // 生成包含当前CA证书的keystore
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            keyStore.load(null, null);
            keyStore.setCertificateEntry("ca", ca);
    
            // 使用包含指定CA证书的keystore生成TrustManager[]数组
            String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
            TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
            tmf.init(keyStore);
            return tmf.getTrustManagers();
        } catch (CertificateException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (KeyStoreException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } finally {
            if (cerInputStream != null) {
                try {
                    cerInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

    4. 初始化SSLContext.

    SSLContext sc = SSLContext.getInstance("SSL");
    TrustManager[] trustManagers = createTrustManager();
    if (trustManagers == null) {
        Log.e("TAG", "tmf create failed!");
        return;
    }
    sc.init(null, trustManagers, new SecureRandom());
    URL url = new URL("https://kyfw.12306.cn/otn/login/init");
    HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());

    Retrofit支持HTTPS

    和普通http客户端请求支持https一样,步骤如下:

    1 CertificateFactory 得到Context.getSocketFactory
    2 添加证书源文件
    3 绑定到okhttpClient
    4设置okhttpClient到retrofit中

    证书同样可以设置到okhttpclient中,我们可以把证书放到raw路径下

    SLSocketFactory sslSocketFactory =getSSLSocketFactory_Certificate(context,"BKS", R.raw.XXX);
    绑定证书:

    protected static SSLSocketFactory getSSLSocketFactory(Context context, int[] certificates) {
    
    if (context == null) {
        throw new NullPointerException("context == null");
    }
    
    CertificateFactory certificateFactory;
    try {
        certificateFactory = CertificateFactory.getInstance("X.509");
        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore.load(null, null);
    
        for (int i = 0; i < certificates.length; i++) {
            InputStream certificate = context.getResources().openRawResource(certificates[i]);
            keyStore.setCertificateEntry(String.valueOf(i), certificateFactory.generateCertificate(certificate));
    
            if (certificate != null) {
                certificate.close();
            }
        }
        SSLContext sslContext = SSLContext.getInstance("TLS");
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(keyStore);
        sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());
       return sslContext.getSocketFactory();

    构建HostnameVerifier:

    protected static HostnameVerifier getHostnameVerifier(final String[] hostUrls) {
    
        HostnameVerifier TRUSTED_VERIFIER = new HostnameVerifier() {
    
            public boolean verify(String hostname, SSLSession session) {
                boolean ret = false;
                for (String host : hostUrls) {
                    if (host.equalsIgnoreCase(hostname)) {
                        ret = true;
                    }
                }
                return ret;
            }
        };
    
    return TRUSTED_VERIFIER;

    设置setSocketFactory:

    okhttpBuilder.socketFactory(HttpsFactroy.getSSLSocketFactory(context, certificates));
    certificates 是你raw下证书源ID, int[] certificates = {R.raw.myssl}

    设置setNameVerifier:

    okhttpBuilder.hostnameVerifier(HttpsFactroy.getHostnameVerifier(hosts));

    hosts是你的host数据 列如 String hosts[]`= {“https//:aaaa,com”, “https//:bbb.com”}

    实现自定义 添加到Retrofit:

    okHttpClient = okhttpBuilder.build(); 
      Retrofit retrofit = new Retrofit.Builder() .client(okHttpClient) .build();
    如果信任所有https请求,
    可以直接将OkHttpClient的HostnameVerifier设置为false

    OkHttpClient client = new OkHttpClient();
    
        client.setHostnameVerifier(new HostnameVerifier() {
            @Override
            public boolean verify(String s, SSLSession sslSession) {
                return true;
            }
        });
        TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
            @Override
            public void checkClientTrusted(
                    java.security.cert.X509Certificate[] x509Certificates,
                    String s) throws java.security.cert.CertificateException {
            }
    
            @Override
            public void checkServerTrusted(
                    java.security.cert.X509Certificate[] x509Certificates,
                    String s) throws java.security.cert.CertificateException {
            }
    
            @Override
            public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                return new java.security.cert.X509Certificate[] {};
            }
        } };
        try {
            SSLContext sc = SSLContext.getInstance("TLS");
            sc.init(null, trustAllCerts, new java.security.SecureRandom());
            client.setSslSocketFactory(sc.getSocketFactory());
        } catch (Exception e) {
            e.printStackTrace();
        }
    
    
             clent.protocols(Collections.singletonList(Protocol.HTTP_1_1))
             .build();

    WebView访问https页面

    服务器证书校验主要针对 WebView 的安全问题。
    在 app 中需要通过 WebView 访问 url,因为服务器采用的自签名证书,而不是 ca 认证,使用 WebView 加载 url 的时候会显示为空白,出现无法加载网页的情况。
    使用 ca 认证的证书,在 WebView 则可以直接显示出来,不需要特殊处理。
    以往针对自签名证书的解决方案是继承 WebViewClient 重写 onReceivedSslError 方法,然后直接使用 handler.proceed(),该方案其实是忽略了证书,存在安全隐患。
    安全的方案是当出现了证书问题的时候,读取 asserts 中保存的的根证书,然后与服务器校验,假如通过了,继续执行 handler.proceed(),否则执行 handler.cancel()。

    简单的解决方案(不安全)

    wv.setWebViewClient(new WebViewClient(){
    
    @override
    public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error){
    
    //handler.cancel(); 默认的处理方式,WebView变成空白页
      handler.proceed();接受证书
    
    //handleMessage(Message msg); 其他处理
    }
    
    // 这行代码一定加上否则效果不会出现  
     webView.getSettings().setJavaScriptEnabled(true);  

    直接运行是页面是可以打开的。但是打好签名包之后,依旧打不开!
    一路追踪之后,发现是那个方法被混淆了
    proguard:mapping.txt
    xx.xx.xxx
        xx.xx.xxx this$0 -> a
        void onReceivedSslError(android.webkit.WebView,android.webkit.SslErrorHandler,android.net.http.SslError) -> onReceivedSslError

    所以还要必要在混淆文件proguard.cfg中,加入以下:
    -keep public class android.net.http.SslError

    -dontwarn android.webkit.WebView
    -dontwarn android.net.http.SslError
    -dontwarn Android.webkit.WebViewClient

    还有一点要提到的是,如果手机添加了代理。也是打不开的。此时,需要将代理的证书导入到Android设备。以Fiddler为例:

    Fiddler本质上是一个HTTPS代理服务器,其自己带的证书显然不会在Android设备的受信任证书列表里。

    有些应用程序会查看服务器端的证书是否是由受信任的根证书签名的,如果不是就直接跳出。

    所以,为了保险起见,我们要将Fiddler代理服务器的证书导到Android设备上。

    导入的过程非常简单,打开设备自带的浏览器,在地址栏中输入代理服务器的IP和端口,例如本例中我们会输入192.169.11.8:8888,进入之后会看到一个Fiddler提供的页面。点击页面中的“FiddlerRoot certificate”链接,接着系统会弹出对话框。输入一个证书名称,然后直接点“确定”就好了。

    但是,如果网页中含有图片,而图片采用的协议是http,那么图片将无法显示。这是因为,

    Android webview 从Lollipop(5.0)开始webview默认不允许混合模式,https当中不能加载http资源,需要设置开启。

    Mixed content using HTTP and HTTPS on WebViews are disabled by default starting Lollipop. Is possible that is not working on devices with Lollipop? If this is the case, you can change the default WebView setting on Lollipop using:

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
    }

    安全的解决方案

    public class WebviewClient3 extends WebViewClient {
        private Context context;
    
        public WebviewClient3(Context context) {
            this.context = context;
        }
    
        @Override
        public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
            test12306(handler, view.getUrl());
        }
    
        // 以 12306 的证书为例,因为 12306 的证书是自签名的
        private void test12306(final SslErrorHandler handler, String url) {
            OkHttpClient.Builder builder;
            try {
                builder = setCertificates(new OkHttpClient.Builder(), context.getAssets().open(MainActivity.cer_protal_root));
            } catch (IOException e) {
                builder = 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) {
                    Log.e("12306 error", e.getMessage());
                    handler.cancel();
                }
    
                @Override
                public void onResponse(Call call, Response response) throws IOException {
                    Log.e("12306 ", response.body().string());
                    handler.proceed();
                }
            });
        }
    
        private OkHttpClient.Builder setCertificates(OkHttpClient.Builder client, InputStream... certificates) {
            try {
                CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
                KeyStore keyStore = KeyStore.getInstance("PKCS12", "BC");
                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) {
                    }
                }
                SSLContext sslContext = SSLContext.getInstance("TLS");
                TrustManagerFactory trustManagerFactory =
                        TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
                trustManagerFactory.init(keyStore);
                sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());
                SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
                X509TrustManager trustManager = Platform.get().trustManager(sslSocketFactory);
                client.sslSocketFactory(sslSocketFactory, trustManager);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return client;
        }
    }

    以上代码可以针对规范的自签名证书进行校验了。但是呢,我们的证书不规范,会出现 Hostname xxx not verified 的情况。这种情况需要对 Hostname 进行校验。需要在 client 上添加如下代码:

    client.hostnameVerifier(new HostnameVerifier() {
            @Override
            public boolean verify(String hostname, SSLSession session) {
                String peerHost = session.getPeerHost();//服务器返回的域名
                try {
                    X509Certificate[] peerCertificates = (X509Certificate[]) session.getPeerCertificates();
                    for (X509Certificate c : peerCertificates) {
                        X500Principal subjectX500Principal = c.getSubjectX500Principal();
                        String name = new X500p(subjectX500Principal).getName();
                        String[] split = name.split(",");
                        for (String s : split) {
                            if (s.startsWith("CN")) {
                                if (s.contains(hostname) && s.contains(peerHost)) {
                                    return true;
                                }
                            }
                        }
                    }
                } catch (SSLPeerUnverifiedException e) {
                    e.printStackTrace();
                }
                return false;
            }
        });


    展开全文
  • webviewHTTPS

    千次阅读 2018-07-09 11:59:17
    WebView webView = new ...//从Android5.0开始,WebView默认不支持同时加载Https和Http混合模式。 //webview加载的网页是http请求的 ,如果网页里有一张图片, //并且该图片的地址是https请求的,这时候用webvie...
  • android https加载WebView图片不显示问题

    千次阅读 2017-08-08 14:44:28
    webview里面加载https url的时候,如果里面需要加载http的资源或者重定向的时候,webview会block页面加载。这是Android 4.4以来google对安全机制的提升。 即当一个安全站点企图加载来自一个不安全站点资源时...
  • Android WebView Https

    2013-06-19 17:05:14
    问题:WebView控件无法加载https协议的url 解决方法:处理SslError webView.setWebViewClient(new WebViewClient(){  @Override  public void onReceivedSslError(WebView view,  SslErrorHandler h
  • Android通过WEBVIEW调用HTTPS
  • webView 加载https

    2016-06-15 13:02:14
    webView 加载https代码块NSURLRequest.h@interface NSURLRequest(SLSSL)+(BOOL)allowsAnyHTTPSCertificateForHost:(NSString*)host;+(void)setAllowsAnyHTTPSCertificate:(BOOL)allow forHost:(NSString*)host; @...
  • android Webview 打开https链接

    万次阅读 2019-06-11 20:24:08
    参考这篇 解决了 打开 https链接... 详细的Webview使用攻略 package com.mycompany.myapp2; import android.content.Context; import android.app.*; import android.os.*; import android.util.*; import andr...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 6,667
精华内容 2,666
关键字:

httpswebview