HTTPS 和 Java 的融合问题
w8822250
8年前
<p>HTTPS 协议是一套完善的标准,它能确保网络连接的安全。要理解这套协议如何运作并非难事,而对应的 RFC 文档 早在 2000 年就有了。</p> <p>尽管 HTTPS 的应用已经如此广泛,你仍然可以遇到一些软件并不对这套协议进行处理,因为它们觉着没必要。不幸的是我曾在使用一种语言实现 认证系统融合 的过程中碰到过这种问题,它本不应该让我感到如此惊讶的。它就是<strong>Java</strong> 。</p> <h2><strong>HTTPS 是如何工作的 ?</strong></h2> <p>在对我遇到的问题进行描述之前,让我先讲讲融合的认证系统是如何运作的。HTTPS 协议使用了 TLS/SSL 协议来确保连接的安全。TLS/SSL 协议对认证的握手环节进行了定义,这个环节能让服务器以一种安全的方式连接到客户端。在 握手 期间会执行如下几个步骤:</p> <ul> <li> <p>客户端发送消息开始进行连接。</p> </li> <li> <p>服务端将证书发送给客户端。</p> </li> <li> <p>客户端使用由受信任的机构签发的证书来对服务端发送的证书进行验证。</p> </li> <li> <p>服务端发送请求,要求客户端提供证书。</p> </li> <li> <p>客户端将它的证书发送给服务端。</p> </li> <li> <p>服务端对客户端发送过来的证书进行验证。</p> </li> <li> <p>服务端和客户端交换主密钥,它会在数据的加密过程中被用到。</p> </li> <li> <p>连接建立。</p> </li> </ul> <p>我与团队伙伴一道尝试用 Java 实现 HTTPS 客户端。结合我们关于 TLS/SSL 握手的知识以及使用 curl 进行手工测试的经验,我们认为要实现这个客户端只需要三个文件: <strong>一个客户端证书,一个客户端私钥</strong> 以及 <strong>一个能对服务端证书进行验证的受信任的证书。</strong></p> <p>好吧,其实我们想错了。</p> <h2><strong>Java: 问题,解决方案,以及为什么如此麻烦</strong></h2> <p>因为每天都使用融合认证这样的做法并不通用,因此我们向这个世界上最好的资源寻求帮助。初看起来 Google 大叔提供的结果并没有显露这个实现背后的复杂性,而且每次查看的搜索结果都在将我们引向越来越令人心烦意乱的解决方案(有些可以追溯到上个世纪90年代)。更糟糕的是我们还的用到 Apache 的 HttpComponents 来实现连接,但是大多数建议使用的方案都是基于纯 <strong>Java</strong> 原生库的。</p> <p>来自互联网的知识让我们确信了如下几点:</p> <ul> <li> <p><strong>Java</strong>不能直接对任何证书或者私钥进行使用(像 curl 那样)。</p> </li> <li> <p><strong>Java</strong>需要使用单独的文件 ( <strong>Java Keystore 文件</strong> ) ,它可以包含原来的证书和密钥。</p> </li> <li> <p>我们需要一个受信任的 keystore 文件 ,里面有服务端对每次 HTTS 连接进行证书验证时需要的证书。</p> </li> <li> <p>我们需要一个密钥的 keysotre 文件,里面有客户端的证书以及用于融合认证的客户端私钥。</p> </li> </ul> <p>首选,我们的创建受信任的 keystore 文件。我们使用 keytool 命令来创建带有证书的 keystore 文件:</p> <pre> <code class="language-java">$ keytool -import -alias trusted_certificate -keystore trusted.jks -file trusted.crt</code></pre> <p>我们在 keystore 文件中将 trusted.jks 以及 证书 trusted.crt 存储在 trusted_certificate 别名下。 在命令的执行过程中,会要求我们输入这个keystore文件的密码,稍后我们会使用这个密码来访问这个 keystore 文件。</p> <p>要创建一个 keystore,还需要几个额外的步骤。在大多数情况下,你可能会要从公司收到两个从客户端证书发出的文件。第一个文件将会是 pem 格式的客户端证书。这个证书将会被发送给服务端。第二个文件是客户端的私钥(也是 pem 格式的),它被用来在握手过程成确认你是否是客户端证书的拥有者。</p> <p>不幸的是, <strong>Java</strong> 只支持 PKCS12 格式,因此我们就得将我们的证书和私钥翻译成 PKCS12 格式的。我们可以使用 OpenSSL来做这件事情。</p> <pre> <code class="language-java">$ openssl pkcs12 -export \ -in client.crt \ -inkey client.key \ -out key.p12 \ -name client</code></pre> <p>我们从 client.crt 和 client.key 文件生成了 key.p12 文件。这里需要再次输入密码。这个密码是要被用来保护私钥的。</p> <p>从 PKCS12 格式的文件我们可以通过将 PKCS12 引入到新的 keystore 中,来生成另外一个 keystore 文件:</p> <pre> <code class="language-java">$ keytool -importkeystore \ -destkeystore key.jks \ -deststorepass <<keystore_password>> \ -destkeypass <<key_password_in_keystore>> \ -alias client \ -srckeystore key.p12 \ -srcstoretype PKCS12 \ -srcstorepass <<original_password_of_PKCS12_file>></code></pre> <p>这个命令看起来更加复杂一点,不过解密过程是一样简单的。在命令的开头我们对新的叫做的 key.jks 的 keystore 的参数进行了声明。我们定义了 keystore 的密码以及私钥的密码,它们都会被 keystore 用到。我们也将私钥分配给了keystore中的一些别名 (在本例中就是 isclient)。接下来,我们对源文件 (key.p12) 进行了指定, 还有文件的格式以及原来的密码。</p> <p>有了 trusted.jks 和 key.jks ,我们就可以开始写代码了。第一步我们得描述一下如何去使用 keystore:</p> <pre> <code class="language-java">File trustedKeystoreFile = new File("trusted.jks"); File keystoreFile = new File("key.jks"); SSLContext sslcontext = SSLContexts.custom() .loadTrustMaterial(trustedKeystoreFile, "<<trusted_keystore_password>>".toCharArray()) .loadKeyMaterial(keystoreFile, "<<keystore_password>>".toCharArray(), "<<original_password_of_PKCS12_file>>".toCharArray()) .build(); SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory( sslcontext, new String[]{"TLSv1.2"}, null, SSLConnectionSocketFactory.getDefaultHostnameVerifier());</code></pre> <p>我们使用了 keystore 文件并且构建了一个 SSL 上下文。接下来,我们创建了 socket 文件,它能为我们的请求提供合适的 HTTPS 连接。</p> <p>最后我们就可以用 Java 来调用我们的端点了:</p> <pre> <code class="language-java">try (CloseableHttpClient httpclient = HttpClients.custom() .setSSLSocketFactory(sslsf) .build()) { HttpGet httpGet = new HttpGet("https://ourserver.com/our/endpoint"); try (CloseableHttpResponse response = httpclient.execute(httGet)) { HttpEntity entity = response.getEntity(); System.out.println(response.getStatusLine()); EntityUtils.consume(entity); } }</code></pre> <p>OK。在创建两个等于于我们原来的证书和私钥的文件(keystore)之后,我们用 Java 实现了融合认证。也许用 Java 实现的 HTTPS 连接有一定的理由,但现在它只会令人头疼。</p> <p> </p> <p>来自:https://www.oschina.net/translate/mutual-problems</p> <p> </p>