将 Apache httpclient 用于 https

Posted

技术标签:

【中文标题】将 Apache httpclient 用于 https【英文标题】:Using Apache httpclient for https 【发布时间】:2011-07-09 12:37:39 【问题描述】:

我在 tomcat 中启用了 https 并拥有一个用于服务器身份验证的自签名证书。我使用 Apache httpClient 创建了一个 http 客户端。我已经设置了一个信任管理器来加载服务器证书。 http客户端可以连接服务器没有问题。要查看发生了什么,我启用了调试:

System.setProperty("javax.net.debug", "ssl");

我看到了以下我完全看不懂的:

***
adding as trusted cert:
  Subject: CN=Me, OU=MyHouse, O=Home, L=X, ST=X, C=BB
  Issuer:  CN=Me, OU=MyHouse, O=Home, L=X, ST=X, C=BB
  Algorithm: RSA; Serial number: 0x4d72356b
  Valid from Sat Mar 05 15:06:51 EET 2011 until Fri Jun 03 16:06:51 EEST 2011 

我的证书显示并添加到信任库(如我所见)。然后:

trigger seeding of SecureRandom
done seeding SecureRandom

这是我没有得到的调试跟踪的部分:

trustStore is: C:\Program Files\Java\jre6\lib\security\cacerts
trustStore type is : jks
trustStore provider is : 
init truststore
adding as trusted cert:
  Subject: CN=SwissSign Platinum CA - G2, O=SwissSign AG, C=CH
  Issuer:  CN=SwissSign Platinum CA - G2, O=SwissSign AG, C=CH
  Algorithm: RSA; Serial number: 0x4eb200670c035d4f
  Valid from Wed Oct 25 11:36:00 EEST 2006 until Sat Oct 25 11:36:00 EEST 2036

adding as trusted cert:
  Subject: EMAILADDRESS=info@valicert.com, CN=http://www.valicert.com/, OU=ValiCert Class 1 Policy Validation Authority, O="ValiCert, Inc.", L=ValiCert Validation Network
  Issuer:  EMAILADDRESS=info@valicert.com, CN=http://www.valicert.com/, OU=ValiCert Class 1 Policy Validation Authority, O="ValiCert, Inc.", L=ValiCert Validation Network
  Algorithm: RSA; Serial number: 0x1
  Valid from Sat Jun 26 01:23:48 EEST 1999 until Wed Jun 26 01:23:48 EEST 2019

好像也使用了默认的java trust store!我的问题是为什么会发生这种情况?

在我的代码中,我明确指定了要使用的特定信任库(通过信任库管理器)。我期望只有这个被使用。似乎我的信任库和 java 的默认值都在使用。这是它应该如何工作的吗?

更新: 我尝试了以下方法:

System.out.println("TMF No:"+tmf.getTrustManagers().length);
System.out.println("Class is "+tmf.getTrustManagers()[0].getClass().getName());  

我认为我应该看到 2 个信任管理器,因为 2 个密钥库(似乎使用了我的和 java 的默认值)。 但结果只有 1 个信托经理!

TMF No:1
Class is com.sun.net.ssl.internal.ssl.X509TrustManagerImpl  

UPDATE2:正如您在下面的代码中看到的,我指定了我的密钥库。我的期望是只应该使用这个(而不是这个 cacert)

    HttpClient client = new DefaultHttpClient();
    SSLContext sslContext = SSLContext.getInstance("TLS");      

    TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    KeyStore ks = KeyStore.getInstance("JKS");
    File trustFile = new File("clientTrustStore.jks");
    ks.load(new FileInputStream(trustFile), null);
    tmf.init(ks);
    sslContext.init(null, tmf.getTrustManagers(),null);  
    SSLSocketFactory sf = new SSLSocketFactory(sslContext); 
    sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
    Scheme scheme = new Scheme("https", sf, 443);
    client.getConnectionManager().getSchemeRegistry().register(scheme);
    httpGet = new HttpGet("https://localhost:8443/myApp");
    HttpResponse httpResponse = client.execute(httpGet);

对我来说没有意义。

【问题讨论】:

【参考方案1】:

我将这个测试应用程序放在一起,使用 Apache HttpClient 包中的 HTTP 测试框架重现问题:

ClassLoader cl = HCTest.class.getClassLoader();
URL url = cl.getResource("test.keystore");
KeyStore keystore  = KeyStore.getInstance("jks");
char[] pwd = "nopassword".toCharArray();
keystore.load(url.openStream(), pwd);

TrustManagerFactory tmf = TrustManagerFactory.getInstance(
        TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keystore);
TrustManager[] tm = tmf.getTrustManagers();

KeyManagerFactory kmfactory = KeyManagerFactory.getInstance(
        KeyManagerFactory.getDefaultAlgorithm());
kmfactory.init(keystore, pwd);
KeyManager[] km = kmfactory.getKeyManagers();

SSLContext sslcontext = SSLContext.getInstance("TLS");
sslcontext.init(km, tm, null);

LocalTestServer localServer = new LocalTestServer(sslcontext);
localServer.registerDefaultHandlers();

localServer.start();
try 

    DefaultHttpClient httpclient = new DefaultHttpClient();
    TrustStrategy trustStrategy = new TrustStrategy() 

        public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException 
            for (X509Certificate cert: chain) 
                System.err.println(cert);
            
            return false;
        

    ;

    SSLSocketFactory sslsf = new SSLSocketFactory("TLS", null, null, keystore, null,
            trustStrategy, new AllowAllHostnameVerifier());
    Scheme https = new Scheme("https", 443, sslsf);
    httpclient.getConnectionManager().getSchemeRegistry().register(https);

    InetSocketAddress address = localServer.getServiceAddress();
    HttpHost target1 = new HttpHost(address.getHostName(), address.getPort(), "https");
    HttpGet httpget1 = new HttpGet("/random/100");
    HttpResponse response1 = httpclient.execute(target1, httpget1);
    System.err.println(response1.getStatusLine());
    HttpEntity entity1 = response1.getEntity();
    EntityUtils.consume(entity1);
    HttpHost target2 = new HttpHost("www.verisign.com", 443, "https");
    HttpGet httpget2 = new HttpGet("/");
    HttpResponse response2 = httpclient.execute(target2, httpget2);
    System.err.println(response2.getStatusLine());
    HttpEntity entity2 = response2.getEntity();
    EntityUtils.consume(entity2);
 finally 
    localServer.stop();

尽管出于某种原因,Sun 的 JSSE 实现似乎总是从默认信任存储中读取信任材料,但它似乎并没有被添加到 SSL 上下文中并且不会影响 SSL 握手期间的信任验证过程。

这是测试应用的输出。如您所见,第一个请求成功,而第二个请求失败,因为与 www.verisign.com 的连接因不受信任而被拒绝。

[
[
  Version: V1
  Subject: CN=Simple Test Http Server, OU=Jakarta HttpClient Project, O=Apache Software Foundation, L=Unknown, ST=Unknown, C=Unknown
  Signature Algorithm: SHA1withDSA, OID = 1.2.840.10040.4.3

  Key:  Sun DSA Public Key
    Parameters:DSA
    p:     fd7f5381 1d751229 52df4a9c 2eece4e7 f611b752 3cef4400 c31e3f80 b6512669
    455d4022 51fb593d 8d58fabf c5f5ba30 f6cb9b55 6cd7813b 801d346f f26660b7
    6b9950a5 a49f9fe8 047b1022 c24fbba9 d7feb7c6 1bf83b57 e7c6a8a6 150f04fb
    83f6d3c5 1ec30235 54135a16 9132f675 f3ae2b61 d72aeff2 2203199d d14801c7
    q:     9760508f 15230bcc b292b982 a2eb840b f0581cf5
    g:     f7e1a085 d69b3dde cbbcab5c 36b857b9 7994afbb fa3aea82 f9574c0b 3d078267
    5159578e bad4594f e6710710 8180b449 167123e8 4c281613 b7cf0932 8cc8a6e1
    3c167a8b 547c8d28 e0a3ae1e 2bb3a675 916ea37f 0bfa2135 62f1fb62 7a01243b
    cca4f1be a8519089 a883dfe1 5ae59f06 928b665e 807b5525 64014c3b fecf492a

  y:
    f0cc639f 702fd3b1 03fa8fa6 676c3756 ea505448 23cd1147 fdfa2d7f 662f7c59
    a02ddc1a fd76673e 25210344 cebbc0e7 6250fff1 a814a59f 30ff5c7e c4f186d8
    f0fd346c 29ea270d b054c040 c74a9fc0 55a7020f eacf9f66 a0d86d04 4f4d23de
    7f1d681f 45c4c674 5762b71b 808ded17 05b74baf 8de3c4ab 2ef662e3 053af09e

  Validity: [From: Sat Dec 11 14:48:35 CET 2004,
               To: Tue Dec 09 14:48:35 CET 2014]
  Issuer: CN=Simple Test Http Server, OU=Jakarta HttpClient Project, O=Apache Software Foundation, L=Unknown, ST=Unknown, C=Unknown
  SerialNumber: [    41bafab3]

]
  Algorithm: [SHA1withDSA]
  Signature:
0000: 30 2D 02 15 00 85 BE 6B   D0 91 EF 34 72 05 FF 1A  0-.....k...4r...
0010: DB F6 DE BF 92 53 9B 14   27 02 14 37 8D E8 CB AC  .....S..'..7....
0020: 4E 6C 93 F2 1F 7D 20 A1   2D 6F 80 5F 58 AE 33     Nl.... .-o._X.3

]
HTTP/1.1 200 OK
[
[
  Version: V3
  Subject: CN=www.verisign.com, OU=" Production Security Services", O="VeriSign, Inc.", STREET=487 East Middlefield Road, L=Mountain View, ST=California, OID.2.5.4.17=94043, C=US, SERIALNUMBER=2497886, OID.2.5.4.15="V1.0, Clause 5.(b)", OID.1.3.6.1.4.1.311.60.2.1.2=Delaware, OID.1.3.6.1.4.1.311.60.2.1.3=US
  Signature Algorithm: SHA1withRSA, OID = 1.2.840.113549.1.1.5

  Key:  Sun RSA public key, 2048 bits
  modulus: 20699622354183393041832954221256409980425015218949582822286196083815087464214375375678538878841956356687753084333860738385445545061253653910861690581771234068858443439641948884498053425403458465980515883570440998475638309355278206558031134532548167239684215445939526428677429035048018486881592078320341210422026566944903775926801017506416629554190534665876551381066249522794321313235316733139718653035476771717662585319643139144923795822646805045585537550376512087897918635167815735560529881178122744633480557211052246428978388768010050150525266771462988042507883304193993556759733514505590387262811565107773578140271
  public exponent: 65537
  Validity: [From: Wed May 26 02:00:00 CEST 2010,
               To: Sat May 26 01:59:59 CEST 2012]
  Issuer: CN=VeriSign Class 3 Extended Validation SSL SGC CA, OU=Terms of use at https://www.verisign.com/rpa (c)06, OU=VeriSign Trust Network, O="VeriSign, Inc.", C=US
  SerialNumber: [    53d2bef9 24a7245e 83ca01e4 6caa2477]

Certificate Extensions: 10
[1]: ObjectId: 1.3.6.1.5.5.7.1.1 Criticality=false
AuthorityInfoAccess [
  [accessMethod: 1.3.6.1.5.5.7.48.1
   accessLocation: URIName: http://EVIntl-ocsp.verisign.com, accessMethod: 1.3.6.1.5.5.7.48.2
   accessLocation: URIName: http://EVIntl-aia.verisign.com/EVIntl2006.cer]
]

...

]
Exception in thread "main" javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated
    at com.sun.net.ssl.internal.ssl.SSLSessionImpl.getPeerCertificates(SSLSessionImpl.java:345)
    at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:128)
    at org.apache.http.conn.ssl.SSLSocketFactory.createLayeredSocket(SSLSocketFactory.java:446)
...

【讨论】:

嗨 Oleg,我遇到了类似的问题,但仅当我发送 POST 请求而不是通过 GET 时。有什么见解吗? 那么,是否有针对此问题提交的错误,我们可以跟踪以寻求解决方案? 我发现我的 JVM 没有解密 4096 位密钥所需的 JCE jar 文件,可能与您正在测试的 2048 位密钥存在相同的问题? 我们如何允许使用 HTTP 客户端在没有证书的情况下连接到 SSL 站点(不安全)?【参考方案2】:

当我使用 Apache HTTP Client 4.3 时,我使用的是池化或基本连接管理器来连接 HTTP 客户端。我注意到,通过使用 java SSL 调试,这些类加载了 cacerts 信任库,而不是我以编程方式指定的那个。

PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
BasicHttpClientConnectionManager cm = new BasicHttpClientConnectionManager();
builder.setConnectionManager( cm );

我想使用它们,但最终删除了它们并创建了一个没有它们的 HTTP 客户端。请注意,builder 是一个 HttpClientBuilder。

我在使用 Java SSL 调试标志运行我的程序时确认,并在调试器中停止。我使用 -Djavax.net.debug=ssl 作为 VM 参数。我在调试器中停止了我的代码,当构造上述任何一个 *ClientConnectionManager 时,将加载 cacerts 文件。

【讨论】:

非常感谢您。我一生都无法弄清楚问题出在哪里,然后在阅读完这篇文章后注意到,如果您传入了连接管理器,构建器基本上会忽略您提供的 sslContext。 这个答案值得更多的爱。我有一个 SSL 和 conn pooling 的任务。我可以发誓,我在 SSL 眼前成功地运行了手动测试和集成测试。然后我做了一些代码清理并漫不经心地插入了池连接管理器。当一切都开始失败时,你可以想象我的痛苦!我的解决方法不是将池值设置为 conn 管理器,实际上可以直接在 httpClient 上设置它,而无需通过 setConnectionManager 实例化和设置。【参考方案3】:

这对我有用:

    KeyStore keyStore  = KeyStore.getInstance("PKCS12");
    FileInputStream instream = new FileInputStream(new File("client-p12-keystore.p12"));
    try 
        keyStore.load(instream, "password".toCharArray());
     finally 
        instream.close();
    

    // Trust own CA and all self-signed certs
    SSLContext sslcontext = SSLContexts.custom()
        .loadKeyMaterial(keyStore, "password".toCharArray())
        //.loadTrustMaterial(trustStore, new TrustSelfSignedStrategy())
        .build();
    // Allow TLSv1 protocol only
    SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
        sslcontext,
        new String[]  "TLSv1" ,
        null,
        SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); //TODO
    CloseableHttpClient httpclient = HttpClients.custom()
        .setHostnameVerifier(SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER) //TODO
        .setSSLSocketFactory(sslsf)
        .build();
    try 

        HttpGet httpget = new HttpGet("https://localhost:8443/secure/index");

        System.out.println("executing request" + httpget.getRequestLine());

        CloseableHttpResponse response = httpclient.execute(httpget);
        try 
            HttpEntity entity = response.getEntity();

            System.out.println("----------------------------------------");
            System.out.println(response.getStatusLine());
            if (entity != null) 
                System.out.println("Response content length: " + entity.getContentLength());
            
            EntityUtils.consume(entity);
         finally 
            response.close();
        
     finally 
        httpclient.close();
    

此代码是http://hc.apache.org/httpcomponents-client-4.3.x/httpclient/examples/org/apache/http/examples/client/ClientCustomSSL.java的修改版

【讨论】:

我们如何在上面的代码中允许连接到 SSL 站点而没有证书不安全的方式? 你用的是哪个版本的HTTPClient? @user4567570 我认为是 4.3.5。要不安全地连接,您需要允许 Hostname Verifier 允许所有,但您还需要指定使用信任所有人的策略:sslContextBuilder.loadTrustMaterial(new TrustAllStrategy());TrustAllStrategy 实现 TrustStrategy,它所做的只是 return true; 嘿,你能指导一下这个***.com/questions/34655031/…吗? 如何获取keystore和密码?【参考方案4】:

根据documentation需要指定密钥库:

Protocol authhttps = new Protocol("https",  
      new AuthSSLProtocolSocketFactory(
          new URL("file:my.keystore"), "mypassword",
          new URL("file:my.truststore"), "mypassword"), 443); 

 HttpClient client = new HttpClient();
 client.getHostConfiguration().setHost("localhost", 443, authhttps);

【讨论】:

我没有使用 commons。我使用的是hc.apache.org/httpcomponents-client-ga。我正在做类似的Scheme scheme = new Scheme("https", 443, sf); client.getConnectionManager().getSchemeRegistry().register(scheme); 和 sf 是 SSLSocketFactory sf = new SSLSocketFactory(sslContext, SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); 并且在 sslContext 中我设置了我的信任存储管理器。如果有帮助,我可以用一些代码更新帖子 @user384706 我有点困惑。问题是 client 正在使用 2 个 TrustManagers(SSLContext.init 说它不应该)或者它是行为奇怪的 tomcat server Tomcat 没有问题。Tomcat 发送我配置的证书。问题出在客户端部分。我指定了一个受信任的信任库,并希望只使用它。调试信息也显示我的因为使用了默认的 java 信任库。我已经更新了我的帖子以在客户端部分显示一些代码 @Cratylus,我在使用自签名证书测试 httpclient 4.0 时遇到了同样的问题。你能发布代码你是如何工作的吗? @DeepeshM:我没有任何工作问题。我的帖子与为什么要打印 cacerts 的证书部分有关。我担心的是 both cacerts 并且我的信任库被使用了

以上是关于将 Apache httpclient 用于 https的主要内容,如果未能解决你的问题,请参考以下文章

httpclient jar 冲突。如何解决?

是否可以使用 Apache HTTPClient 4.2.5 发出 HTTP/2 请求?

如何在Apache HttpClient中设置TLS版本

HttpClient实现调用外部项目接口工具类

Java如何获取创蓝253短信验证码?

如何使用 Apache HttpClient 4 获取文件上传的进度条?