很多文章对客户端https的使用都是很模糊的,不但如此,有些开发者直接从网上拷贝一些使用https的“漏洞”代码,无形之中让客户端处在一种高风险的情况下。

今天我们就对有关https使用的问题进行深入的探讨,希望能解决以往的困惑。对于https,需要了解其工作原理的可以参考https是如何工作的?,更多关于https的问题我会站在客户端的角度在后面陆陆续续的写出来。


证书锁定

简介

首先来说说什么是证书锁定。

证书锁定是用来限制哪些证书和证书颁发机构是可信任的。需要我们直接在代码中固定写死使用某个服务器的证书,然后用自定义的信任存储去代替系统系统自带的,再去连接我们的服务器,我们将这种做法称之为证书锁定。换言之,证书锁定就是在代码中验证当前服务器是否持有某张指定的证书,如果不是则强行断开链接。

有同学问证书锁定有什么好处么?最大的好处使用证书锁定提高安全性,降低了成本。为什么这么说呢?如果你想破解该通信,需要首先拿到客户端,然后对其反编译,修改后再重新打包签名,相比原先的做法,这无疑是增加了破解难度。除了之外,由于证书锁定可以使用自签名的证书,那就意味着我们不需要再向android认可的证书颁发机构购买证书了,这样就可以剩下每年1000多块钱的证书费用,能省一点就省一点嘛。

retrofit中使用证书锁定

现在,我们来看看如何在retrofit中进行证书锁定。

OkHttpClient client = new OkHttpClient.Builder().certificatePinner(new CertificatePinner.Builder().add("sbbic.com", "sha1/C8xoaOSEzPC6BgGmxAt/EAcsajw=").add("closedevice.com", "sha1/T5x9IXmcrQ7YuQxXnxoCmeeQ84c=").build())

通过上面的代码不难看出,retrofit中的证书锁定同样是借助OkHttpClient实现的:通过为OkHttpClient添加CertificatePinner即可。CertificatePinner对象以构建器的方式创建,可以通过其add()方法来锁定多个证书。

证书锁定的原理

现在我们深入下证书锁定的原理。我们知道,无论http还是https,都是服务端被动,客户端主动。那么问题来了,客户端第一次发出请求之后,无法确定服务端是不是合法的。那么很可能就会出现以下情景:

正常情况下是这样,我们想要根据文章aid查看某篇文章内容,其流程如下:

此时,如果黑客恶意拦截这个通信过程,会是怎么样?

此时恶意服务端完全可以发起双向攻击:对上可以欺骗服务端,对下可以欺骗客户端,更严重的是客户端段和服务端完全感知不到已经被攻击了。这就是所谓的中间人攻击。

中间人攻击(MITM攻击)是指,黑客拦截并篡改网络中的通信数据。又分为被动MITM和主动MITM,被动MITM只窃取通信数据而不修改,而主动MITM不当能窃取数据,还会篡改通信数据。最常见的中间人攻击常常发生了公共wifi或者公共路由上,有兴趣的私下可以问我,这里不做演示了。

现在可以看看证书锁定是怎么样提高安全性,避免中间人攻击的,用一张简单的流程图来说明:

不难看出,通过证书锁定能有有效的避免中间人攻击。

证书锁定的缺点

证书锁定尽管带了较高的安全性,但是这种安全性的提高却牺牲了灵活性。一旦当证书发生变化时,我们的客户端也必须随之升级,除此之外,我们的服务端不得不为了兼容以前的客户端而做出一些妥协或者说直接停用以前的客户端,这对开发者和用户来说并不是那么的友好。

但实际上,极少情况下我们才会变动证书。因此,如果产品安全性要求比较高还是启动证书锁定吧。


使用android认可的证书颁发机构颁发的证书

有些同学可能好奇自己公司中使用https,但是在客户端代码中并没有书写绑定证书的代码?以访问github的代码为例:

public void loadData() {Retrofit retrofit = new Retrofit.Builder().baseUrl("https://api.github.com/").build();GitHubApi api = retrofit.create(GitHubApi.class);Call<ResponseBody> call = api.contributorsBySimpleGetCall(mUserName, mRepo);call.enqueue(new Callback<ResponseBody>() {@Overridepublic void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {// handle response}@Overridepublic void onFailure(Call<ResponseBody> call, Throwable t) {// handle failure}});}

在https是如何工作的一文中我们说过android已经帮我们预置了150多个证书,这些证书你可以在设置->安全->信任的凭据中看到(在windows中,你可以在命令行中打开certmgr.msc来打开证书管理器,这里你可以看看windows预置的证书)。现在可以明白了,之所以没有内置证书的原因在于:我们服务端用的证书是从android认可的证书颁发机构购买的证书,在android中已经内置了这些证书,而默认情况下,retrofit 2.0 所依赖的okhttp 3.0 是信任它们,因此可以直接访问而无需在客户端设置什么。


使用非android认可的证书颁发机构颁发的证书或自签名证书

购买证书毕竟是花钱的,现在免费的证书有少之又少,因此使用自签名证书就是另外一种常见的方式了。什么是自签名证书呢?所谓的自签名证书就是没有通过受信任的证书颁发机构,自己给自己颁发的证书(下文中,我将非android认可的证书颁发机构颁发的证书也归为自签名证书)。最典型的就是12306火车购票,使用的证书就不是受信任的证书颁发机构颁发的,而是旗下SRCA(中铁数字证书认证中心,简称中铁CA,它是铁道自己搞的机构,因此相当于自己给自己颁发证书)颁发的证书,如下图:

SSL证书分为三类:
1. 由android认可的证书颁发机构或者该结构下属的机构颁发的证书,比如Symantec,Go Daddy等机构,约150多个。更多的自行在手机“设置->安全->信任的凭据”中查看

2.没有被android所认可的证书所颁发的证书
3. 自己颁发的证书

这三类证书中,只有第一种在使用中不会出现安全提示,不会抛出异常。

由于我们使用的是自签名的证书,因此客户端不信任服务器,会抛出异常:javax.net.ssl.SSLHandshakeException:.为此,我们需要自定义信任处理器(TrustManager)来替代系统默认的信任处理器,这样我们才能正常的使用自定义的正说或者非android认可的证书颁发机构颁发的证书。

针对使用场景,又分为以下两种情况:一种是安全性要求不高的情况下,客户端无需内置证书;另外一种则是客户端内置证书。
下面我会针对这两种情况说明其中一些问题点。

客户端不内置证书

我们知道由于我们使用的是自签名的证书,所以需要自定义TrustManager,那么很多开发者的处理策略非常简单粗暴:让客户端不对服务器证书做任何验证,其实现代码如下:

public static SSLSocketFactory getSSLSocketFactory() throws Exception {//创建一个不验证证书链的证书信任管理器。final TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {@Overridepublic void checkClientTrusted(java.security.cert.X509Certificate[] chain,String authType) throws CertificateException {}@Overridepublic void checkServerTrusted(java.security.cert.X509Certificate[] chain,String authType) throws CertificateException {}@Overridepublic java.security.cert.X509Certificate[] getAcceptedIssuers() {return new java.security.cert.X509Certificate[0];}}};// Install the all-trusting trust managerfinal SSLContext sslContext = SSLContext.getInstance("TLS");sslContext.init(null, trustAllCerts,new java.security.SecureRandom());// Create an ssl socket factory with our all-trusting managerreturn sslContext.getSocketFactory();}//使用自定义SSLSocketFactoryprivate void onHttps(OkHttpClient.Builder builder) {try {builder.sslSocketFactory(getSSLSocketFactory()).hostnameVerifier(org.apache.http.conn.ssl.SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);} catch (Exception e) {e.printStackTrace();}}

上面的代码不要轻易的应用在实际工程中,除非你能容忍他的危害。为什么这么说呢?继续往下看。

在上面的代码中,我们自行实现X509TrustManager时并没有对其中三个核心的方法进行 具体实现(主要是没有在checkServerTrusted()验证证书),这样做相当于直接忽略了检验服务端证书。因此无论服务器的证书如何,都能建立起https链接。

看起来好像解决了我们的问题,实则带来更大的危害。此时,虽然能建立HTTPS连接,但是无形之中间人攻击打开了一道门。此时,黑客完全可以拦截到我们的HTTPS请求,然后用伪造的证书冒充真正服务端的数字证书,由于客户端不对证书做验证(也就没法判断服务端到底是正常的还是伪造的),这样客户端就会和黑客的服务器建立连接。这就相当于你以为你对的对面是个美女,却没有想到已经被掉包了,想想“狸猫换太子”就明白了。(对这点不明白的同学,可以参见证书锁定中的示例。)

那么怎么避免呢?我们需要在自定义TrustManager时重写checkServerTrusted()方法,并在该方法中校验证书,完善后的代码如下:

    public static SSLSocketFactory getSSLSocketFactory() throws Exception {// Create a trust manager that does not validate certificate chainsfinal TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {//证书中的公钥public static final String PUB_KEY = "3082010a0282010100d52ff5dd432b3a05113ec1a7065fa5a80308810e4e181cf14f7598c8d553cccb7d5111fdcdb55f6ee84fc92cd594adc1245a9c4cd41cbe407a919c5b4d4a37a012f8834df8cfe947c490464602fc05c18960374198336ba1c2e56d2e984bdfb8683610520e417a1a9a5053a10457355cf45878612f04bb134e3d670cf96c6e598fd0c693308fe3d084a0a91692bbd9722f05852f507d910b782db4ab13a92a7df814ee4304dccdad1b766bb671b6f8de578b7f27e76a2000d8d9e6b429d4fef8ffaa4e8037e167a2ce48752f1435f08923ed7e2dafef52ff30fef9ab66fdb556a82b257443ba30a93fda7a0af20418aa0b45403a2f829ea6e4b8ddbb9987f1bf0203010001";@Overridepublic void checkClientTrusted(java.security.cert.X509Certificate[] chain,String authType) throws CertificateException {}//客户端并为对ssl证书的有效性进行校验@Overridepublic void checkServerTrusted(java.security.cert.X509Certificate[] chain,String authType) throws CertificateException {if (chain == null) {throw new IllegalArgumentException("checkServerTrusted:x509Certificate array isnull");}if (!(chain.length > 0)) {throw new IllegalArgumentException("checkServerTrusted: X509Certificate is empty");}if (!(null != authType && authType.equalsIgnoreCase("RSA"))) {throw new CertificateException("checkServerTrusted: AuthType is not RSA");}// Perform customary SSL/TLS checkstry {TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");tmf.init((KeyStore) null);for (TrustManager trustManager : tmf.getTrustManagers()) {((X509TrustManager) trustManager).checkServerTrusted(chain, authType);}} catch (Exception e) {throw new CertificateException(e);}// Hack ahead: BigInteger and toString(). We know a DER encoded Public Key begins// with 0×30 (ASN.1 SEQUENCE and CONSTRUCTED), so there is no leading 0×00 to drop.RSAPublicKey pubkey = (RSAPublicKey) chain[0].getPublicKey();String encoded = new BigInteger(1 /* positive */, pubkey.getEncoded()).toString(16);// Pin it!final boolean expected = PUB_KEY.equalsIgnoreCase(encoded);if (!expected) {throw new CertificateException("checkServerTrusted: Expected public key: "+ PUB_KEY + ", got public key:" + encoded);}}@Overridepublic java.security.cert.X509Certificate[] getAcceptedIssuers() {return new java.security.cert.X509Certificate[0];}}};// Install the all-trusting trust managerfinal SSLContext sslContext = SSLContext.getInstance("TLS");sslContext.init(null, trustAllCerts,new java.security.SecureRandom());// Create an ssl socket factory with our all-trusting managerreturn sslContext.getSocketFactory();}

其中PUB_KEY是我们证书中的公钥,你可以自行从自己的证书中提取。我们看到,在checkServerTrusted()方法中,我们通过证书的公钥信息来确认证书的真伪,如果验证失败,则中断请求。当然,此处加入证书的有效期会更加的完善,实现起来比较简单,这里就不做说明了。

除了上面那种在checkServerTrusted()实现证书验证的方式之外,我们也可以利用retrofit中CertificatePinner来实现证书锁定,同样也能达到我们的目的。

客户端内置证书

如果我们使用的是自签名证书,那么客户端中的retrofit该如何进行设置呢?关键还是我们上文提到的TrustManager。在retrofit中使用自签名证书大致要经过以下几步:

  1. 将证书添加到工程中
  2. 自定义信任管理器TrustManager
  3. 用自定义TrustManager代替系统默认的信任管理器

我们按步骤进行说明。

添加证书到工程

比如现在我们有个证书media.bks,首先需要将其放在res/raw目录下,当然你可以可以放在assets目录下。

我们知道java本身支持的证书格式jks,但是遗憾的是在android当中并不支持jks格式正式,而是需要bks格式的证书。因此我们需要将jks证书转换成bks格式证书,关于jks转bks不再本文做重点说明

自定义TrustManager

和上面不同的是,这里需要实现本地证书的加载,具体见代码:

 protected static SSLSocketFactory getSSLSocketFactory(Context context, int[] certificates) {if (context == null) {throw new NullPointerException("context == null");}//CertificateFactory用来证书生成CertificateFactory certificateFactory;try {certificateFactory = CertificateFactory.getInstance("X.509");//Create a KeyStore containing our trusted CAsKeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());keyStore.load(null, null);for (int i = 0; i < certificates.length; i++) {//读取本地证书InputStream is = context.getResources().openRawResource(certificates[i]);keyStore.setCertificateEntry(String.valueOf(i), certificateFactory.generateCertificate(is));if (is != null) {is.close();}}//Create a TrustManager that trusts the CAs in our keyStoreTrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());trustManagerFactory.init(keyStore);//Create an SSLContext that uses our TrustManagerSSLContext sslContext = SSLContext.getInstance("TLS");sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());return sslContext.getSocketFactory();} catch (Exception e) {}return null;}

用自定义TrustManager代替系统默认的信任管理器

   private void onHttpCertficates(OkHttpClient.Builder builder) {int[] certficates = new int[]{R.raw.media};builder.socketFactory(getSSLSocketFactory(AppContext.context(), certficates));}

这样我们就可以的客户端就可以使用自签名的证书了。其实不难发现,使用非android认证证书颁发机构颁发的证书的关键在于:修改android中SSLContext自带的TrustManager以便能让我们的签名通过验证。


暂告一段落

本文中简单首先介绍了证书锁定的使用、原理及优缺点,接着对客户端使用自定义证书中的一些点做了介绍,希望能帮助各位打造安全的安卓客户端。

另外,大多数情况下,我建议使用证书锁定来提高安全性。关于双向证书验证,后续有时间再补充。

Retrofit中如何正确的使用https?相关推荐

  1. java 获取动态的service_【Android】动态代理在 Retrofit 中的使用

    首先,什么是动态代理和为什么会有动态代理. 众所周知,Java 是一门静态语言,编写完的类,无法在运行时做动态修改. 一个简单的动态代理如下: 1.先定义一个接口,想要使用动态代理,必须先定义一个接口 ...

  2. Day 27: Restify —— 在Node.js中构建正确的REST Web服务

    今天决定学一个叫做restify的Node.js模块.restify模块使得在Node.js中写正确的REST API变得容易了很多,而且它还提供了即装即用的支持,如版本控制.错误处理.CORS和内容 ...

  3. Android实战处理带+号的电话号码在Arabic语言中的正确显示

    2019独角兽企业重金招聘Python工程师标准>>> 现在有串电话号码+8613212345678(注意是带+号的),要保证在Arabic 语言中的正确显示,如何来做? 要求: 英 ...

  4. JavaScript中错误正确处理方式,你用对了吗?

    JavaScript的事件驱动范式增添了丰富的语言,也是让使用JavaScript编程变得更加多样化.如果将浏览器设想为JavaScript的事件驱动工具,那么当错误发生时,某个事件就会被抛出.理论上 ...

  5. java面试题32:Java网络程序设计中,下列正确的描述是()

    java面试题32:Java网络程序设计中,下列正确的描述是() A:Java网络编程API建立在Socket基础之上 B:Java网络接口只支持tcP以及其上层协议 C:Java网络接口只支持UDP ...

  6. html没有内容怎么爬,Url没有在网页中返回正确的html(对于我的Java爬虫)

    我想从网页上下载一些图像,为此我正在编写爬网程序.我测试了这个页面的几个抓取工具,但没有工作,因为我想.Url没有在网页中返回正确的html(对于我的Java爬虫) 第一步,我收集了770+相机型号( ...

  7. 如何在回调中访问正确的“ this”?

    本文翻译自:How to access the correct `this` inside a callback? I have a constructor function which regist ...

  8. 代码中如何让无序标记的内容并排_英语技术文档中如何正确使用无序列表和有序列表?...

    Foreword 之前跟大家分享过英语技术文档中如何正确使用时态和英语技术文档中如何正确使用人称,这一篇再跟大家分享一下如何正确使用无序列表和有序列表. 其实,在技术文档中,除了无序列表和有序列表,另 ...

  9. Retrofit中关于CallAdapter使用的设计模式分析

    引言 CallAdapter的使用 CallAdapter中的适配器模式 CallAdapter中的工厂方法模式 Retrofit使用策略模式匹配合适的CallAdapter 总结 Retrofit作 ...

最新文章

  1. 从扫描序列的标准化做起,西门子医疗正在中国布这样一盘棋...
  2. 大数据分析工资单:六大行员工再涨薪 人均年薪超26万
  3. 【推荐系统】协同过滤 零基础到入门
  4. mysql数据库入门教程(15):流程控制结构
  5. 前端学习(1601):状态提升
  6. python3 beautifulsoup 模块详解_关于beautifulsoup模块的详细介绍
  7. 看完这篇文章,还不懂nginx,算我输
  8. IOS 开发 UIProgress 和 UISlidre 进度条和滑动条组件
  9. php 显示下拉菜单,PHP在下拉列表中显示菜单树
  10. Fedora32升级Fedora33后无线网络无法连接的问题
  11. Android 字体ttf文件下载(含github下载地址)
  12. 小程序列表左滑效果在IOS呈上下滑动影响样式
  13. 英语口语必备900句
  14. StackStorm安装WebUI
  15. php中strpos什么意思,PHP使用strpos()和strrpos()定位文本
  16. 【山无遮,海无拦】LeetCode题集 线性枚举之最值算法
  17. 中国十大无线耳机排行榜,音质好配置高的蓝牙耳机分享
  18. 2. ZK客户端与服务端建立连接的过程(基于NIO)
  19. 深度学习-扩张卷积(dilated convolution)
  20. html登录错误有提示,为什么我登录之后的提示老是网页上有错误呢?

热门文章

  1. 平台“运营+变现”万精油方法论:拉新→促活→留存→转化→裂变→提频
  2. 小程序中的block
  3. MEM/MBA数学基础(03)整式与分式 运算
  4. 求三角形的外接圆圆心个半径
  5. Cmake编译配置opencv3.3+contrib+cuda7.5
  6. 多元线性回归系数求解
  7. 阿里云AI训练营第一天
  8. 程序员自我修炼:《匠艺整洁之道》读书总结
  9. uiautomator2安卓测试框架报No tests found for given includes
  10. 树莓派:基于物联网的指纹打卡器