第14课:如何用

第14课:如何用 Java 代码访问 HTTPS 的应用

前面的一些章节内容主要分享了如何使用 XCA、OpenSSL、KeyTool 等第三方工具来生成和管理 SSL 证书以及其私钥,并用这些工具进行组合,配置了 Tomcat、IIS、Nginix 等服务器的 HTTPS 应用。 本节内容将会结合笔者在实际项目中用 Java 调用 HTTPS 的时候遇到的问题和坑,和大家分享一下如何见招拆招。

环境准备

因为笔者准备用 httpClient 来调用 HTTPS 的应用,所以创建的是 Maven 工程,让 Maven 来帮助我们自动进行包的管理和下载,当然也能用 Gradle。下面是我将要用到的 Maven 的依赖。

<dependencies>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.4.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpmime</artifactId>
            <version>4.4.1</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.7</version>
        </dependency>
    </dependencies>

另外,笔者将会结合一个我用私有 CA 签发的 HTTPS 网站,和大家分享调用私有 CA 签发的网站和调用普通公网网站,比如百度、腾讯、京东等网站的区别。网站的地址详见这里

enter image description here

调用百度的 HTTPS 网站

现在笔者以调用百度的网为例子,访问百度的主页

代码如下:

package com.talkdocter.ssl;

import java.io.IOException;
import java.util.Date;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

public class HttpsInvokeService {

  private String url;

  public HttpsInvokeService(String url){
    this.url=url;
  }

  public String invoke(){
    CloseableHttpClient httpClient=HttpClients.createDefault();
    HttpGet httpGet=new HttpGet(url);
    ResponseHandler<String> responseHandler=new ResponseHandler<String>() {

      public String handleResponse(HttpResponse httpResponse) throws ClientProtocolException, IOException {
        int status=httpResponse.getStatusLine().getStatusCode();
        if(status>=200 && status <300){
          HttpEntity httpEntity=httpResponse.getEntity();
          if(null!=httpEntity){
            String result=EntityUtils.toString(httpEntity);
            return result;
          }else{
            return null;
          }
        }else{
          throw new ClientProtocolException("Unexpected Response status:" + status);
        }

      }

    };

    try {
      String responseBody= httpClient.execute(httpGet,responseHandler);
      if(null!=responseBody){
       System.out.println(responseBody);
      }

    } catch (ClientProtocolException e) {
      e.printStackTrace();
    } catch (IOException e) {
      e.printStackTrace();
    } finally{
      if(null!=httpClient){
         try {
          httpClient.close();
         } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }
    return null;
  }

  public String getUrl() {
    return url;
  }
  public void setUrl(String url) {
    this.url = url;
  }


}

单元测试代码如下:

package com.talkdocter.ssl;
import org.junit.Test;
public class HttpsInvokeServiceTest {

  @Test
  public void invokeBaiduHttpsURL(){
    String url="https://www.baidu.com/"; 
    HttpsInvokeService httpsInvokeService=new HttpsInvokeService(url);
    httpsInvokeService.invoke();
  }

}

运行测试后,其输出结果如下:

enter image description here

从上面的输出结果来看,完全成功。

调用有私有 CA 签发的 HTTPS 站点

下面以调用由私有 CA 签发的服务器的 SSL 证书搭建的站点:https://iis-web-01/ 为例子,来看看这次我们还有这么幸运吗?在调用之前,为了先给大家一个心理准备,先把 https://iis-web-01/ 证书打开,让大家看看。

enter image description here

从上面可以看出,其是由 51TalkDocter Root CA 私有 CA 颁发的。下面开始调用,注意,实现的代码没有任何改变,现在改变的是其目标的 URL 地址,测试代码如下:

import org.junit.Test;

public class HttpsInvokeServiceTest {

  @Test
  public void invokePrivateCAHttpsURL(){
    String url="https://iis-web-01/";
    HttpsInvokeService httpsInvokeService=new HttpsInvokeService(url);
    httpsInvokeService.invoke();
  }

}

只是把 URL 地址从 String url="https://www.baidu.com/" 改成了 String url="https://iis-web-01/"; 结果却调用异常了,具体信息如下。

javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
    at sun.security.ssl.Alerts.getSSLException(Alerts.java:192)
    at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1949)
    at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:302)
    at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:296)
    at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1509)
    at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:216)
    at sun.security.ssl.Handshaker.processLoop(Handshaker.java:979)
    at sun.security.ssl.Handshaker.process_record(Handshaker.java:914)
    at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1062)
    at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1375)
    at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1403)
    at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1387)

由此可见,是在客户端调用 HTTPS 网站并验证 HTTPS 网的证书的时候,验证没有通过。那为什么,访问百度网址的时候却成功了呢?

答案就在 JRE 的 Security 目录下有一个 cacerts 文件,这个文件里面预存储了市场一些著名的商业 CA 的证书。

enter image description here

Java 在调用 HTTPS 网站站点的时候,会先遍历这个 cacerts 文件,看 HTTPS 网站传过来的证书的根 CA 是否在这个文件里存在,如果存在,则认为当前的网站站点是可信任的,否则就认为是不可信任的,就会抛出校验 SSL 证书异常。下面是百度的证书,其是由 GlobalSign Root CA -R1 签署的。

enter image description here

下面咱们到 cacerts 文件里面看一看,看其是否在里面。

C:\>keytool -list -keystore "C:\Program Files\Java\jdk1.8.0_101\jre\lib\security
\cacerts" > c:\cacert.txt
Enter keystore password:  changeit

笔者这里把其打印的信息重定向到了 c:\cacert.txt 文件,打开 c:\cacert.txt 文件, 打开 c:\cacert.txt 文件,搜索其根证书CA的指纹:B1:BC:96:8B:D4:F4:9D:62:2A:A8:9A:81:F2:15:01:52:A4:1D:82:9C 还真搜索到了。

enter image description here

还真在 c:\cacert.txt 文件搜索到了。

enter image description here

所以访问百度站点的时候就能访问成功。

聪明的读者也许已经想到了,那如果把 https://iis-web-01/ 站点的私有 CA 的根证书导入到 JDK 的 cacerts 文件中,那岂不是就可以了。恭喜你!你的思路是对的,假设:

  • 根 CA 证书放置在下面的位置:C:\ssldemo\51TalkDocter_Root_CA.cer
  • JRE 的路径为:C:\Program Files\Java\jdk1.8.0_101\jre\lib\security\cacerts。

导入命令如下:

keytool import -trustcacerts -alias 51talkdocterCA -file "C:\ssldemo\51TalkDocter_Root_CA.cer" -keystore "C:\Program Files\Java\jdk1.8.0_101\jre\lib\security\cacerts" -storepass changeit

在输出的命令行中,对话框中输入“Y”,导入完成。

enter image description here

再次调用 https://iis-web-01/ 站点,访问结果如下:

enter image description here

成功调用由私有 CA 颁发的签署的站点 https://iis-web-01/。

使用 JCE 支持 AES256

在 Java 中,默认支持 AES128,如果要使用 AES256,还需要替换一下两个文件。Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files。为什么需要用 JCE,是因为美国对这种高强度加密的算法实现有出口限制,所以默认情况下是支持 AES 128 位。

为了形象的说明这个问题,笔者在互联网上找了一个站点,https://pleiades.stoa.org/。

如果用 OpenSSL 的s_client访问这个站点的时候,我们会发现其服务器的对称秘钥使用的算法是 AES256,命令如下:

s_client -connect pleiades.stoa.org:443

输出结果如下:

enter image description here

其用于 TLS 通信的密码套件使用的是 ECDHE-RSA-AES256-GCM-SHA384,其中里面就有 AES256,如果直接使用下面的代码调用这个站点:

@Test
  public void testWeChatAPI(){
    String url="https://pleiades.stoa.org/";
    HttpsInvokeService httpsInvokeService=new HttpsInvokeService(url);
    httpsInvokeService.invoke();
  }

其将会抛出下面的类似异常:

javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure
    at sun.security.ssl.Alerts.getSSLException(Alerts.java:192)
    at sun.security.ssl.Alerts.getSSLException(Alerts.java:154)
    at sun.security.ssl.SSLSocketImpl.recvAlert(SSLSocketImpl.java:2023)
    at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1125)
    at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1375)
    at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1403)
    at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1387)
    at org.apache.http.conn.ssl.SSLConnectionSocketFactory.createLayeredSocket(SSLConnectionSocketFactory.java:394)
    at org.apache.http.conn.ssl.SSLConnectionSocketFactory.connectSocket(SSLConnectionSocketFactory.java:353)
...

为了解决这个问题,我们需要根据本地使用的 Java JRE 的版本去下面的站点,下载相应的 JCE 文件。

JCE 下载地址:

本机测试环境是 JRE 8,下载完后可看到这两个 jar 包:local_policy.jar 和US_export_policy.jar,然后在拷贝到C:\Program Files\Java\jdk1.8.0_101\jre\lib\security目录下覆盖已有的两个 jar 包。

enter image description here

替换后,我们发现调用成功了!

总结

本文通过创建一个 Maven 项目,然后使用 HttpClient 第三方开源库分别调用百度站点和由私有 CA 签署和颁发的 SSL 服务器证书的站点进行了调用结果的比较,出乎意料的是调用百度站点成功了,但是调用由私有 CA 签署和颁发的 SSL 服务器证书的站点的时候失败了,并把失败的根本原因进行了深入的分析,并给出了解决方案。

紧接着我们用实际能够访问的网站地址 https://pleiades.stoa.org/,重现了当 Java 调用 TLS 握手协议时,如果 TLS 通信的密码套件使用的是 ECDHE-RSA-AES256-GCM-SHA384(其中包含了 AES256 算法)的时候,其将会调用失败。其本质是因为美国对高强度加密算法的出口限制,防止其他国家用于军事目的。为了解决这个问题,我们可以通过到 Java 的官方网站下载相应 JDK 版本对应的 JCE 文件来解决这个问题。

希望这篇文章能够大家以后调用 HTTPS 站点应用的时候能够起到抛砖引玉的作用。

上一篇
下一篇
目录