欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

浅析Android系统中HTTPS通信的实现

程序员文章站 2024-03-05 12:24:54
前言 最近有一个跟https相关的问题需要解决,因此花时间学习了一下android平台https的使用,同时也看了一些https的原理,这里分享一下学习心得。 http...

前言
最近有一个跟https相关的问题需要解决,因此花时间学习了一下android平台https的使用,同时也看了一些https的原理,这里分享一下学习心得。

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通信,就是不加密的通信,所有的信息明文传播,带来了三大风险:
1. 窃听风险:第三方可以获知通信内容。
2. 篡改风险:第三方可以修改通知内容。
3. 冒充风险:第三方可以冒充他人身份参与通信。
ssl/tls协议是为了解决这三大风险而设计的,希望达到:
1. 所有信息都是加密传输,第三方无法窃听。
2. 具有校验机制,一旦被篡改,通信双方都会立刻发现。
3. 配备身份证书,防止身份被冒充。
基本的运行过程
ssl/tls协议的基本思路是采用公钥加密法,也就是说,客户端先向服务器端索要公钥,然后用公钥加密信息,服务器收到密文后,用自己的私钥解密。但是这里需要了解两个问题的解决方案。
1. 如何保证公钥不被篡改?
解决方法:将公钥放在数字证书中。只要证书是可信的,公钥就是可信的。
2. 公钥加密计算量太大,如何减少耗用的时间?
解决方法:每一次对话(session),客户端和服务器端都生成一个“对话密钥”(session key),用它来加密信息。由于“对话密钥”是对称加密,所以运算速度非常快,而服务器公钥只用于加密“对话密钥”本身,这样就减少了加密运算的消耗时间。
因此,ssl/tls协议的基本过程是这样的:
1. 客户端向服务器端索要并验证公钥。
2. 双方协商生成“对话密钥”。
3. 双方采用“对话密钥”进行加密通信。
上面过程的前两布,又称为“握手阶段”。
握手阶段的详细过程

浅析Android系统中HTTPS通信的实现

握手阶段”涉及四次通信,需要注意的是,“握手阶段”的所有通信都是明文的。
客户端发出请求(clienthello)
首先,客户端(通常是浏览器)先向服务器发出加密通信的请求,这被叫做clienthello请求。在这一步中,客户端主要向服务器提供以下信息:
1. 支持的协议版本,比如tls 1.0版
2. 一个客户端生成的随机数,稍后用于生成“对话密钥”。
3. 支持的加密方法,比如rsa公钥加密。
4. 支持的压缩方法。
这里需要注意的是,客户端发送的信息之中不包括服务器的域名。也就是说,理论上服务器只能包含一个网站,否则会分不清应用向客户端提供哪一个网站的数字证书。这就是为什么通常一台服务器只能有一张数字证书的原因。
服务器回应(serverhello)
服务器收到客户端请求后,向客户端发出回应,这叫做serverhello。服务器的回应包含以下内容:
1. 确认使用的加密通信协议版本,比如tls 1.0版本。如果浏览器与服务器支持的版本不一致,服务器关闭加密通信。
2. 一个服务器生成的随机数,稍后用于生成“对话密钥”。
3. 确认使用的加密方法,比如rsa公钥加密。
4. 服务器证书。
除了上面这些信息,如果服务器需要确认客户端的身份,就会再包含一项请求,要求客户端提供“客户端证书”。比如,金融机构往往只允许认证客户连入自己的网络,就会向正式客户提供usb密钥,里面就包含了一张客户端证书。
客户端回应
客户端收到服务器回应以后,首先验证服务器证书。如果证书不是可信机构颁发,或者证书中的域名与实际域名不一致,或者证书已经过期,就会向访问者显示一个警告,由其选择是否还要继续通信。
如果证书没有问题,客户端就会从证书中取出服务器的公钥。然后,向服务器发送下面三项消息。
1. 一个随机数。该随机数用服务器公钥加密,防止被窃听。
2. 编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。
3. 客户端握手结束通知,表示客户端的握手阶段已经结束。这一项通常也是前面发送的所有内容的hash值,用来供服务器校验。
上面第一项随机数,是整个握手阶段出现的第三个随机数,又称“pre-master key”。有了它以后,客户端和服务器就同时有了三个随机数,接着双方就用事先商定的加密方法,各自生成本次会话所用的同一把“会话密钥”。
服务器的最后回应
服务器收到客户端的第三个随机数pre-master key之后,计算生成本次会话所用的“会话密钥”。然后,向客户端最后发送下面信息。
1. 编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。
2. 服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时也是前面发生的所有内容的hash值,用来供客户端校验。
握手结束
至此,整个握手阶段全部结束。接下来,客户端与服务器进入加密通信,就完全是使用普通的http协议,只不过用“会话密钥”加密内容。

服务器基于nginx搭建https虚拟站点
之前一篇文章详细介绍了在服务器端如何生成ssl证书,并基于nginx搭建https服务器,链接:nginx搭建https服务器

android实现https通信
由于各种原因吧,这里使用httpclicent类讲解一下android如何建立https连接。代码demo如下。
mainactivity.java

   

 package com.example.photocrop; 
   
  import java.io.bufferedreader; 
  import java.io.inputstreamreader; 
   
  import org.apache.http.httpresponse; 
  import org.apache.http.httpstatus; 
  import org.apache.http.statusline; 
  import org.apache.http.client.httpclient; 
  import org.apache.http.client.methods.httppost; 
  import org.apache.http.client.methods.httpurirequest; 
   
  import android.app.activity; 
  import android.os.asynctask; 
  import android.os.bundle; 
  import android.os.asynctask.status; 
  import android.text.textutils; 
  import android.util.log; 
  import android.view.view; 
  import android.widget.button; 
  import android.widget.textview; 
   
  public class mainactivity extends activity { 
    private button httpsbutton; 
    private textview contextview; 
   
    private createhttpsconntask httpstask; 
   
    @override 
    protected void oncreate(bundle savedinstancestate) { 
      super.oncreate(savedinstancestate); 
      setcontentview(r.layout.activity_main); 
   
      httpsbutton = (button) findviewbyid(r.id.create_https_button); 
      httpsbutton.setonclicklistener(new view.onclicklistener() { 
   
        @override 
        public void onclick(view v) { 
          runhttpsconnection(); 
        } 
      }); 
   
      contextview = (textview) findviewbyid(r.id.content_textview); 
      contextview.settext("初始为空"); 
    } 
   
    private void runhttpsconnection() { 
      if (httpstask == null || httpstask.getstatus() == status.finished) { 
        httpstask = new createhttpsconntask(); 
        httpstask.execute(); 
      } 
    } 
   
    private class createhttpsconntask extends asynctask<void, void, void> { 
      private static final string https_example_url = "自定义"; 
      private stringbuffer sbuffer = new stringbuffer(); 
   
      @override 
      protected void doinbackground(void... params) { 
        httpurirequest request = new httppost(https_example_url); 
        httpclient httpclient = httputils.gethttpsclient(); 
        try { 
          httpresponse httpresponse = httpclient.execute(request); 
          if (httpresponse != null) { 
            statusline statusline = httpresponse.getstatusline(); 
            if (statusline != null 
                && statusline.getstatuscode() == httpstatus.sc_ok) { 
              bufferedreader reader = null; 
              try { 
                reader = new bufferedreader(new inputstreamreader( 
                    httpresponse.getentity().getcontent(), 
                    "utf-8")); 
                string line = null; 
                while ((line = reader.readline()) != null) { 
                  sbuffer.append(line); 
                } 
   
              } catch (exception e) { 
                log.e("https", e.getmessage()); 
              } finally { 
                if (reader != null) { 
                  reader.close(); 
                  reader = null; 
                } 
              } 
            } 
          } 
   
        } catch (exception e) { 
          log.e("https", e.getmessage()); 
        } finally { 
   
        } 
   
        return null; 
      } 
   
      @override 
      protected void onpostexecute(void result) { 
        if (!textutils.isempty(sbuffer.tostring())) { 
          contextview.settext(sbuffer.tostring()); 
        } 
      } 
   
    } 
  } 

httputils.java

   

package com.example.photocrop; 
   
  import org.apache.http.httpversion; 
  import org.apache.http.client.httpclient; 
  import org.apache.http.conn.clientconnectionmanager; 
  import org.apache.http.conn.scheme.plainsocketfactory; 
  import org.apache.http.conn.scheme.scheme; 
  import org.apache.http.conn.scheme.schemeregistry; 
  import org.apache.http.conn.ssl.sslsocketfactory; 
  import org.apache.http.impl.client.defaulthttpclient; 
  import org.apache.http.impl.conn.tsccm.threadsafeclientconnmanager; 
  import org.apache.http.params.basichttpparams; 
  import org.apache.http.params.httpprotocolparams; 
  import org.apache.http.protocol.http; 
   
  import android.content.context; 
   
   
  public class httputils { 
    public static httpclient gethttpsclient() { 
      basichttpparams params = new basichttpparams(); 
      httpprotocolparams.setversion(params, httpversion.http_1_1); 
      httpprotocolparams.setcontentcharset(params, http.default_content_charset); 
      httpprotocolparams.setuseexpectcontinue(params, true); 
       
      schemeregistry schreg = new schemeregistry(); 
      schreg.register(new scheme("http", plainsocketfactory.getsocketfactory(), 80)); 
      schreg.register(new scheme("https", sslsocketfactory.getsocketfactory(), 443)); 
       
      clientconnectionmanager connmgr = new threadsafeclientconnmanager(params, schreg); 
       
      return new defaulthttpclient(connmgr, params); 
    } 
     
    public static httpclient getcustomclient() { 
      basichttpparams params = new basichttpparams(); 
      httpprotocolparams.setversion(params, httpversion.http_1_1); 
      httpprotocolparams.setcontentcharset(params, http.default_content_charset); 
      httpprotocolparams.setuseexpectcontinue(params, true); 
       
      schemeregistry schreg = new schemeregistry(); 
      schreg.register(new scheme("http", plainsocketfactory.getsocketfactory(), 80)); 
      schreg.register(new scheme("https", mysslsocketfactory.getsocketfactory(), 443)); 
       
      clientconnectionmanager connmgr = new threadsafeclientconnmanager(params, schreg); 
       
      return new defaulthttpclient(connmgr, params); 
    } 
     
    public static httpclient getspecialkeystoreclient(context context) { 
      basichttpparams params = new basichttpparams(); 
      httpprotocolparams.setversion(params, httpversion.http_1_1); 
      httpprotocolparams.setcontentcharset(params, http.default_content_charset); 
      httpprotocolparams.setuseexpectcontinue(params, true); 
       
      schemeregistry schreg = new schemeregistry(); 
      schreg.register(new scheme("http", plainsocketfactory.getsocketfactory(), 80)); 
      schreg.register(new scheme("https", customersocketfactory.getsocketfactory(context), 443)); 
       
      clientconnectionmanager connmgr = new threadsafeclientconnmanager(params, schreg); 
       
      return new defaulthttpclient(connmgr, params); 
    } 
  } 

activity_main.xml

   

 <linearlayout xmlns:android="http://schemas.android.com/apk/res/android" 
    xmlns:tools="http://schemas.android.com/tools" 
    android:layout_width="match_parent" 
    android:layout_height="match_parent" 
    android:orientation="vertical"> 
   
    <button 
      android:id="@+id/create_https_button" 
      android:layout_width="match_parent" 
      android:layout_height="wrap_content" 
      android:text="@string/hello_world" 
      android:textsize="16sp" /> 
   
    <textview 
      android:id="@+id/content_textview" 
      android:layout_width="match_parent" 
      android:layout_height="wrap_content" 
      android:gravity="center" 
      android:textsize="16sp" /> 
   
  </linearlayout> 

android使用defaulthttpclient建立https连接,关键需要加入对https的支持:

  schreg.register(new scheme("https", sslsocketfactory.getsocketfactory(), 443)); 

加入对https的支持,就可以有效的建立https连接了,例如“https://www.google.com.hk”了,但是访问自己基于nginx搭建的https服务器却不行,因为它使用了不被系统承认的自定义证书,会报出如下问题:no peer certificate。
使用自定义证书并忽略验证的https连接方式
解决证书不被系统承认的方法,就是跳过系统校验。要跳过系统校验,就不能再使用系统标准的ssl socketfactory了,需要自定义一个。然后为了在这个自定义ssl socketfactory里跳过校验,还需要自定义一个trustmanager,在其中忽略所有校验,即trustall。

   

package com.example.photocrop; 
   
  import java.io.ioexception; 
  import java.net.socket; 
  import java.net.unknownhostexception; 
  import java.security.keymanagementexception; 
  import java.security.keystore; 
  import java.security.keystoreexception; 
  import java.security.nosuchalgorithmexception; 
  import java.security.unrecoverablekeyexception; 
  import java.security.cert.certificateexception; 
  import java.security.cert.x509certificate; 
   
  import javax.net.ssl.sslcontext; 
  import javax.net.ssl.trustmanager; 
  import javax.net.ssl.x509trustmanager; 
  import org.apache.http.conn.ssl.sslsocketfactory; 
   
  public class mysslsocketfactory extends sslsocketfactory { 
    sslcontext sslcontext = sslcontext.getinstance("tls"); 
   
    public mysslsocketfactory(keystore truststore) 
        throws nosuchalgorithmexception, keymanagementexception, 
        keystoreexception, unrecoverablekeyexception { 
      super(truststore); 
      trustmanager tm = new x509trustmanager() { 
   
        @override 
        public x509certificate[] getacceptedissuers() { 
          return null; 
        } 
   
        @override 
        public void checkservertrusted(x509certificate[] chain, 
            string authtype) throws certificateexception { 
   
        } 
   
        @override 
        public void checkclienttrusted(x509certificate[] chain, 
            string authtype) throws certificateexception { 
   
        } 
      }; 
   
      sslcontext.init(null, new trustmanager[] { tm }, null); 
    } 
   
    @override 
    public socket createsocket() throws ioexception { 
      return sslcontext.getsocketfactory().createsocket(); 
    } 
   
    @override 
    public socket createsocket(socket socket, string host, int port, 
        boolean autoclose) throws ioexception, unknownhostexception { 
      return sslcontext.getsocketfactory().createsocket(socket, host, port, 
          autoclose); 
    } 
   
    public static sslsocketfactory getsocketfactory() { 
      try { 
        keystore truststore = keystore.getinstance(keystore 
            .getdefaulttype()); 
        truststore.load(null, null); 
        sslsocketfactory factory = new mysslsocketfactory(truststore); 
        return factory; 
      } catch (exception e) { 
        e.getmessage(); 
        return null; 
      } 
    } 
  } 

同时,需要修改defaulthttpclient的register方法,改为自己构建的sslsocket:

 

  public static httpclient getcustomclient() { 
    basichttpparams params = new basichttpparams(); 
    httpprotocolparams.setversion(params, httpversion.http_1_1); 
    httpprotocolparams.setcontentcharset(params, http.default_content_charset); 
    httpprotocolparams.setuseexpectcontinue(params, true); 
     
    schemeregistry schreg = new schemeregistry(); 
    schreg.register(new scheme("http", plainsocketfactory.getsocketfactory(), 80)); 
    schreg.register(new scheme("https", mysslsocketfactory.getsocketfactory(), 443)); 
     
    clientconnectionmanager connmgr = new threadsafeclientconnmanager(params, schreg); 
     
    return new defaulthttpclient(connmgr, params); 
  } 

这样就可以成功的访问自己构建的基于nginx的https虚拟站点了。
缺陷:
不过,虽然这个方案使用了https,客户端和服务器端的通信内容得到了加密,嗅探程序无法得到传输的内容,但是无法抵挡“中间人攻击”。例如,在内网配置一个dns,把目标服务器域名解析到本地的一个地址,然后在这个地址上使用一个中间服务器作为代理,它使用一个假的证书与客户端通讯,然后再由这个代理服务器作为客户端连接到实际的服务器,用真的证书与服务器通讯。这样所有的通讯内容都会经过这个代理,而客户端不会感知,这是由于客户端不校验服务器公钥证书导致的。

使用自定义证书建立https连接
为了防止上面方案可能导致的“中间人攻击”,我们可以下载服务器端公钥证书,然后将公钥证书编译到android应用中,由应用自己来验证证书。
生成keystore

要验证自定义证书,首先要把证书编译到应用中,这需要使用keytool工具生产keystore文件。这里的证书就是指目标服务器的公钥,可以从web服务器配置的.crt文件或.pem文件获得。同时,你需要配置bouncycastle,我下载的是bcprov-jdk16-145.jar,至于配置大家自行google就好了。

  keytool -importcert -v -trustcacerts -alias example -file www.example.com.crt -keystore example.bks -storetype bks -providerclass org.bouncycastle.jce.provider.bouncycastleprovider -providerpath /home/wzy/downloads/java/jdk1.7.0_60/jre/lib/ext/bcprov-jdk16-145.jar -storepass pw123456 

运行后将显示证书内容并提示你是否确认,输入y回车即可。

生产keystore文件成功后,将其放在app应用的res/raw目录下即可。
使用自定义keystore实现连接
思路和trushall差不多,也是需要一个自定义的sslsokcetfactory,不过因为还需要验证证书,因此不需要自定义trustmanager了。

   

 package com.example.photocrop; 
   
  import java.io.ioexception; 
  import java.io.inputstream; 
  import java.security.keymanagementexception; 
  import java.security.keystore; 
  import java.security.keystoreexception; 
  import java.security.nosuchalgorithmexception; 
  import java.security.unrecoverablekeyexception; 
   
  import org.apache.http.conn.ssl.sslsocketfactory; 
   
  import android.content.context; 
   
  public class customersocketfactory extends sslsocketfactory { 
   
    private static final string passwd = "pw123456"; 
   
    public customersocketfactory(keystore truststore) 
        throws nosuchalgorithmexception, keymanagementexception, 
        keystoreexception, unrecoverablekeyexception { 
      super(truststore); 
    } 
   
    public static sslsocketfactory getsocketfactory(context context) { 
      inputstream input = null; 
      try { 
        input = context.getresources().openrawresource(r.raw.example); 
        keystore truststore = keystore.getinstance(keystore 
            .getdefaulttype()); 
   
        truststore.load(input, passwd.tochararray()); 
   
        sslsocketfactory factory = new customersocketfactory(truststore); 
   
        return factory; 
      } catch (exception e) { 
        e.printstacktrace(); 
        return null; 
      } finally { 
        if (input != null) { 
          try { 
            input.close(); 
          } catch (ioexception e) { 
            e.printstacktrace(); 
          } 
          input = null; 
        } 
      } 
    } 
   
  } 

同时,需要修改defaulthttpclient的register方法,改为自己构建的sslsocket:

 

  public static httpclient getspecialkeystoreclient(context context) { 
    basichttpparams params = new basichttpparams(); 
    httpprotocolparams.setversion(params, httpversion.http_1_1); 
    httpprotocolparams.setcontentcharset(params, http.default_content_charset); 
    httpprotocolparams.setuseexpectcontinue(params, true); 
     
    schemeregistry schreg = new schemeregistry(); 
    schreg.register(new scheme("http", plainsocketfactory.getsocketfactory(), 80)); 
    schreg.register(new scheme("https", customersocketfactory.getsocketfactory(context), 443)); 
     
    clientconnectionmanager connmgr = new threadsafeclientconnmanager(params, schreg); 
     
    return new defaulthttpclient(connmgr, params); 
  }