Android 偶遇 HTTPS

yaozhuan 8年前
   <p> </p>    <p>HTTPS ,该来的总要来的。</p>    <p>最近领导对移动端开发提出了很多优化的要求啊!其中一点就是数据安全性,之前安卓后端接口一直是用的HTTP,那么我想了想,HTTPS应该是入门级的了,赶紧找资料整理了下!</p>    <p>对于向权威机构申请过证书的网络地址,用 <a href="/misc/goto?guid=4958964956476581901" rel="nofollow,noindex">OkHttp</a> 或者 <a href="/misc/goto?guid=4959672988753017190" rel="nofollow,noindex">HttpsURLConnection</a> 都可以直接访问,不需要做额外的事情。但是申请证书要$$的,所以开发的时候我们接口经常是使用自签名证书,或者即使上线了也还是用自签名的,因为安卓用到的基本都是数据接口,又不会用浏览器访问,不想付钱不行咩!</p>    <h2>访问自签名网址</h2>    <h2>使用keytool生成证书</h2>    <p>keytool是JDK提供的管理加密密钥、X.509证书链和可信证书密钥库的简便工具。安卓开发必定安装了JDK并且一般都会配置好环境变量,所以你可以直接在终端或DOC窗口输入 keytool 命令来查看帮助。</p>    <p>1.生成密钥对</p>    <pre>  <code class="language-java">keytool -genkey -alias server -keyalg RSA -keystore server.jks  </code></pre>    <p>-alias 后面跟的是唯一别名, -keystore 后面填保存秘钥对的文件路径</p>    <p>还可以添加一个 -validity 天数 声明有效期</p>    <p>需要注意的地方:执行命令之后第一个问题让你输入名字的地方最好设置成 <strong>域名</strong> ,比如这样 baidu.com 或者这样 localhost ,反正匹配你要调式的域名就对了,当然,如果你在安卓上调试,那么本地地址可能用不了。</p>    <p>2.导出证书</p>    <p>上面生成了服务端使用的密钥对,现在可以通过它生成证书给客户端使用</p>    <pre>  <code class="language-java">keytool -export -alias server -storepass 123456 -keystore server.jks -file server.cer  </code></pre>    <p>-storepass 后面跟的是你刚才设置的密码,不加这个也没关系,它会主动问你!; -file 设置了保存证书的路径</p>    <h2>服务端配置</h2>    <p>这里我使用tomcat8进行测试,它的配置很简单,修改tomcat目录下的conf/server.xml文件,添加如下内容,这里设置了端口号为8443</p>    <pre>  <code class="language-java"><Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"              maxThreads="150" SSLEnabled="true" scheme="https" secure="true"              clientAuth="false" sslProtocol="TLS"               keystoreFile="密钥库文件路径,也就是.jks文件"              keystorePass="密码" />  </code></pre>    <h2>安卓端配置</h2>    <ul>     <li><strong>加载证书</strong></li>    </ul>    <p>把之前生成的证书(.cer)放到安卓项目的 assets 或者 raw 目录下,读取文件流用以下方法获取SSLSocketFactory 。</p>    <pre>  <code class="language-java">public static SSLSocketFactory getSslSocketFactory(InputStream certificates)      {          SSLContext sslContext = null;          try          {              CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");                Certificate ca;              try {                  ca = certificateFactory.generateCertificate(certificates);                } finally {                  certificates.close();              }                // Create a KeyStore containing our trusted CAs              String keyStoreType = KeyStore.getDefaultType();              KeyStore keyStore = KeyStore.getInstance(keyStoreType);              keyStore.load(null, null);              keyStore.setCertificateEntry("ca", ca);                // Create a TrustManager that trusts the CAs in our KeyStore              String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();              TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);              tmf.init(keyStore);                // Create an SSLContext that uses our TrustManager              sslContext = SSLContext.getInstance("TLS");              sslContext.init(null, tmf.getTrustManagers(), null);            } catch (Exception e)          {              e.printStackTrace();          }            return sslContext != null? sslContext.getSocketFactory():null;      }  </code></pre>    <h2>OkHttp</h2>    <p>在OkHttp中使用很简单,获取SSLSocketFactory之后通过OkHttp的构建方法传入就行了。</p>    <p>使用的OkHttp版本是 3.2.0 。</p>    <pre>  <code class="language-java">OkHttpClient client = new OkHttpClient.Builder()                  .sslSocketFactory(sslSocketFactory)                  .build();  </code></pre>    <p>设置完之后你就可以访问该证书对应的域名地址了,不需要别的附加操作了。</p>    <h2>HttpsURLConnection</h2>    <p>OkHttp的API与安卓中默认提供的URLConnection是很接近的,所以配置也是如出一辙。</p>    <pre>  <code class="language-java">URL url = new URL("https://....");    HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection();    httpsURLConnection.setSSLSocketFactory(sslSocketFactory);  </code></pre>    <h2>双向验证</h2>    <p>双向认证需要两个密钥实体,一个放服务端一个放客户端。前面我们已经实现单向的认证,现在只需要给客户端生成一个密钥库,并且让服务端信任客户端就可以了。</p>    <p>生成客户端密钥</p>    <pre>  <code class="language-java">keytool -genkey -alias android -keyalg RSA -keystore android.jks  </code></pre>    <p>导出客户端证书(字符串形式)</p>    <pre>  <code class="language-java">keytool -keystore android.jks -alias android -exportcert -rfc > android.pem  </code></pre>    <p>将导出的证书添加信任到服务端的密钥库</p>    <pre>  <code class="language-java">keytool -importcert -trustcacerts -alias android -keystore server.jks -file android.pem  </code></pre>    <h2>服务端配置</h2>    <p>修改tomcat目录下的conf/server.xml文件</p>    <pre>  <code class="language-java"><Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"              maxThreads="150" SSLEnabled="true" scheme="https" secure="true"              sslProtocol="TLS"               keystoreFile="密钥库文件路径,也就是.jks文件"              keystorePass="密码"              //修改两条内容,其它和之前单向认证一样就行              clientAuth="true"              truststoreFile="和keystoreFile填一样" />  </code></pre>    <h2>安卓端配置</h2>    <p>刚才生成了客户端的密钥库 android.jks 。但是安卓默认是不支持jks格式的!比较常规的解决方式是用 <a href="/misc/goto?guid=4959672988852131636" rel="nofollow,noindex">Portecle</a> 工具将它转换成bks文件。</p>    <p><a href="/misc/goto?guid=4959672988945865171" rel="nofollow,noindex">点这里下载Portecle工具</a></p>    <p>下载完之后解压并在目录下运行命令: java -jar portecle.jar 或者也可以直接双击它打开</p>    <p>运行之后就会出来UI界面,用它打开 android.jks 然后选菜单 Tools –> Change Keystore Type –> BKS 在弹出框输入密码进行转换,最后别忘记选菜单 File –> Save Keystore As 将它另存为 android.kbs (名字随意)</p>    <p>可能会出现这样的异常:java.security.KeyStoreException: java.io.IOException: Error initialising store of key store</p>    <p>解决办法是下载 <a href="/misc/goto?guid=4958964328276339715" rel="nofollow,noindex">JCE</a> 然后替换掉 JDK\jre\lib\security 和 JRE\lib\security 这两个目录下的同名文件,并重启Portecle</p>    <p>生成kbs文件之后,把它放到安卓的目录下 assets 或者 raw 。</p>    <p>然后把获取SSLSocketFactory的方法改成下面这样</p>    <pre>  <code class="language-java">public static SSLSocketFactory getSslSocketFactory          (InputStream certificates,InputStream key,String keyPassword)  {      SSLContext sslContext = null;      try      {          CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");            Certificate ca;          try {              ca = certificateFactory.generateCertificate(certificates);            } finally {              certificates.close();          }            String keyStoreType = KeyStore.getDefaultType();          KeyStore keyStore = KeyStore.getInstance(keyStoreType);          keyStore.load(null, null);          keyStore.setCertificateEntry("ca", ca);            String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();          TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);          tmf.init(keyStore);            String keyStoreType2 = "BKS";          KeyStore keyStore2 = KeyStore.getInstance(keyStoreType2);          keyStore2.load(key, keyPassword.toCharArray());            String kmfAlgorithm = KeyManagerFactory.getDefaultAlgorithm();          KeyManagerFactory kmf = KeyManagerFactory.getInstance(kmfAlgorithm);          kmf.init(keyStore2,keyPassword.toCharArray());            sslContext = SSLContext.getInstance("TLS");          sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);        } catch (Exception e)      {          e.printStackTrace();      }        return sslContext != null? sslContext.getSocketFactory():null;  }  </code></pre>    <p>上面的都改完之后,双向验证的配置就完成了。你可以打开浏览器访问下你配置好的地址,应该不能访问,提示你: 不接受您的登录证书,或者您的登录证书可能已过期 。因为你的系统没有加入刚才生成的客户端密钥库,安卓端像上面一样设置完SSLSocketFactory就可以正常访问了。</p>    <p>额外的,我把获取SSLSocketFactory的方法封装了下 <a href="/misc/goto?guid=4959672989073507212" rel="nofollow,noindex">HttpsUtil</a> ,用 getSslSocketFactory 方法就行了</p>    <p>来自: <a href="/misc/goto?guid=4959672989161291163" rel="nofollow">http://blog.majiajie.me/2016/05/11/Android-偶遇HTTPS/</a></p>