使用Apache httpclient进行https
我已经在tomcat中启用了https,并且有一个用于服务器authentication的自签名证书。 我使用Apache httpClient创build了一个http客户端。 我已经设置了一个加载服务器证书的信任pipe理器。 http客户端可以连接服务器没有问题。 要查看发生了什么,我启用了debugging:
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
这里是从debugging痕迹我没有得到的部分:
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信任存储! 我的问题是为什么会发生这种情况?
在我的代码中,我明确指定了一个特定的信任存储(通过truststoremanagers)来使用。 我期待只有这个被使用。 看来,我的信任库和Java的默认使用。 这是应该如何工作?
更新:
我尝试了以下内容:
System.out.println("TMF No:"+tmf.getTrustManagers().length); System.out.println("Class is "+tmf.getTrustManagers()[0].getClass().getName());
我以为我应该看到2个信任pipe理器,因为2个密钥库(我的和java的默认情况似乎被使用)。
但结果只有一个信任pipe理器!
TMF No:1 Class is com.sun.net.ssl.internal.ssl.X509TrustManagerImpl
UPDATE2:正如你在代码下面看到的,我指定了我的keystore.My的期望是,只有这个应该使用(不是这个和 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);
对我来说没有意义。
我把这个testing应用程序放在一起,使用Apache HttpClient包中的HTTPtesting框架来重现问题:
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(); }
尽pipeSun的JSSE实现似乎总是从默认信任存储中读取信任材料,但似乎并未将其添加到SSL上下文中,并影响SSL握手过程中的信任validation过程。
这是testing应用程序的输出。 正如你所看到的,第一个请求成功,而第二个失败,因为http://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: 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) ...
这对我来说是有效的:
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(); } }
当我使用Apache HTTP Client 4.3时,我使用池连接pipe理器或基本连接pipe理器连接到HTTP客户端。 我注意到,从使用Java SSLdebugging,这些类加载cacerts信任存储,而不是我已经程序指定的。
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(); BasicHttpClientConnectionManager cm = new BasicHttpClientConnectionManager(); builder.setConnectionManager( cm );
我想使用它们,但最终删除它们并创build一个没有它们的HTTP客户端。 请注意,构build器是一个HttpClientBuilder。
我使用Java SSLdebugging标志运行我的程序时已确认,并在debugging器中停止。 我使用-Djavax.net.debug = ssl作为VM参数。 我在debugging器中停止了我的代码,当上面的* ClientConnectionManager被构build时,cacerts文件将被加载。
根据文档,您需要指定密钥存储区:
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);