如何通过 HttpsURLConnection 使用证书身份验证?

Posted

技术标签:

【中文标题】如何通过 HttpsURLConnection 使用证书身份验证?【英文标题】:How can I use certificate authentication with HttpsURLConnection? 【发布时间】:2012-01-10 11:18:48 【问题描述】:

我正在尝试连接到 HTTPS URL,但我需要使用客户端身份验证和第三方软件放置在我系统上的证书。

我完全不知道我应该如何找到或使用它,我所要做的只是 C# 示例代码,它与我找到的所有 Java 答案都有很大不同。 (例如,KeyStore 显然需要某种密码?)

这是我的 C# 示例代码

System.Security.Cryptography.X509Certificates.X509CertificateCollection SSC_Certs = 
    new System.Security.Cryptography.X509Certificates.X509CertificateCollection();

Microsoft.Web.Services2.Security.X509.X509CertificateStore WS2_store =
    Microsoft.Web.Services2.Security.X509.X509CertificateStore.CurrentUserStore(
    Microsoft.Web.Services2.Security.X509.X509CertificateStore.MyStore);

WS2_store.OpenRead();

Microsoft.Web.Services2.Security.X509.X509CertificateCollection WS2_store_Certs = WS2_store.Certificates;

然后它只是遍历 WS2_store_Certs CertificateCollection 并一直检查它们。 再进一步,它会像这样设置证书:

HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(url_string);
httpWebRequest.ClientCertificates = SSC_Certs;

这一切看起来都很合乎逻辑,即使我不知道它是如何找到证书的,但我仍然无法找到与之对应的 Java。

更新

我正在建立的连接是依赖于 JDK 5 的更大应用程序的一部分,但我设法只使用 sunmscapi jar 来查找我正在寻找的证书。但是,当我尝试使用 Windows 密钥库进行连接时,它会出错,所以我认为我通过从 Windows 商店获取所需的证书并将其插入到默认的 java 中来解决了这个问题。现在我得到一个 EOFException ,然后是一个 SSLHandshakeException 说“握手期间远程主机关闭连接”。 ssl 调试跟踪不会立即向我显示问题,因为我需要的证书显示在此处的证书链中。

它做了整个 ClientKeyExchange 的事情,说它已经完成,然后我从调试日志中得到的最后一条消息是

[write] MD5 and SHA1 hashes:  len = 16
0000: 14 00 00 0C D3 E1 E7 3D   C2 37 2F 41 F9 38 26 CC  .......=.7/A.8&.
Padded plaintext before ENCRYPTION:  len = 32
0000: 14 00 00 0C D3 E1 E7 3D   C2 37 2F 41 F9 38 26 CC  .......=.7/A.8&.
0010: CB 10 05 A1 3D C3 13 1C   EC 39 ED 93 79 9E 4D B0  ....=....9..y.M.
AWT-EventQueue-1, WRITE: TLSv1 Handshake, length = 32
[Raw write]: length = 37
0000: 16 03 01 00 20 06 B1 D8   8F 9B 70 92 F4 AD 0D 91  .... .....p.....
0010: 25 9C 7D 3E 65 C1 8C A7   F7 DA 09 C0 84 FF F4 4A  %..>e..........J
0020: CE FD 4D 65 8D                                     ..Me.
AWT-EventQueue-1, received EOFException: error

我用来建立连接的代码是

KeyStore jks = KeyStore.getInstance(KeyStore.getDefaultType());
jks.load(null, null);

KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
jks.setCertificateEntry("alias", cert1); //X509Certificate obtained from windows keystore
kmf.init(jks, new char[0]);

SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(kmf.getKeyManagers(), new TrustManager[]tm, null);

sslsocketfactory = sslContext.getSocketFactory();
System.setProperty("https.proxyHost", proxyurl);
System.setProperty("https.proxyPort", proxyport);

Authenticator.setDefault(new MyAuthenticator("proxyID", "proxyPassword"));
URL url = new URL(null, urlStr, new sun.net.www.protocol.https.Handler());
HttpsURLConnection uc = (HttpsURLConnection) url.openConnection();
uc.setSSLSocketFactory(sslsocketfactory);
uc.setAllowUserInteraction(true);
uc.setRequestMethod("POST");
uc.connect();

(我还没有尝试过 HttpClient,因为我不知道如何找到证书文件,而且我也不确定这在每个客户端系统上是否总是相同的。)

另一个更新

我已经在 WINDOWS-ROOT 密钥库中找到了我需要的证书的 CA(并使用 .verify() 检查它们是否都已签出),我也已将它们添加到 java 密钥库中但仍然没有什么变化。我猜他们应该进入 TrustStore,但我还没有找到以编程方式执行此操作的方法。 (我宁愿不依赖最终用户来做这种事情,因为我可以向他们保证的是,由于这个可笑的长问题开头提到的第三方软件,证书和 CA 将存在。)

还有更多更新

加上之前的更新,我得出的结论是我的问题一定在于我的 CA 不在 Java 的 cacerts 文件中,因此它从服务器获取受信任的 CA 列表,但没有识别它们并随后不发回导致连接失败的单个证书。 所以问题仍然存在,如何让 Java 将其密钥库用作信任库或以编程方式将证书添加到 cacerts(无需文件路径)?因为如果这些都不可能,那只会给我留下秘密选项 C,voodoo。我会开始用针刺杜克娃娃,以防万一。

【问题讨论】:

您的目标是使用用户 Windows 密钥库中的证书吗?我认为在 Java 中没有这样做的方法,但是您可以使用 keytool 创建一个单独的密钥库。 【参考方案1】:

好的,所以您的问题的标题是如何使用 HttpsURLConnection 进行证书身份验证? 我有一个工作示例。要让它工作,它有一个先决条件:

    您必须有一个密钥库,其中包含您的证书文件。 (我知道您的情况并不完全允许这样做,但请在这里关注我,以便我们可以稍微缩小您的问题范围,因为这有点太复杂,无法立即回答。)

所以,首先要掌握实际的证书文件。如果您使用的是 Windows 7,则可以通过以下步骤完成:

    打开Internet Explorer, 打开 Tools(在 Internet Explorer 9 中是 cog 图标), 单击Internet 选项, 转到内容标签, 点击证书, 找到手头的证书, 单击它并单击导出并将其保存到文件(DER 编码的二进制 X.509)。

(导出后将其从其他证书中删除,确保Java不会以某种方式使用它。我不知道它是否可以使用它,但它不能伤害。)

现在,您必须创建一个密钥存储文件并将导出的证书导入其中,可以通过以下方式完成。

> keytool -importcert -file <certificate> -keystore <keystore> -alias <alias>

显然,keytool 必须在你的工作路径上。它是 JDK 的一部分。

它会提示您输入密码(密钥库的密码;它与证书无关任何操作),我不知道如何将其设置为""现在,所以设置为password 或其他。

之后,通过以下步骤,您可以通过代理建立与端点的安全连接。

    首先,加载密钥库文件。

    InputStream trustStream = new FileInputStream("path/to/<keystore>");
    char[] trustPassword = "<password>".toCharArray();
    

    初始化一个KeyStore

    KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
    trustStore.load(trustStream, trustPassword);
    

    初始化 TrustManager 对象。 (我认为这些处理证书解析或类似的东西,但就我而言,这很神奇。

    TrustManagerFactory trustFactory =
        TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    trustFactory.init(trustStore);
    TrustManager[] trustManagers = trustFactory.getTrustManagers();
    

    创建一个新的 SSLContext,将 TrustManager 对象加载到其中并将其设置为默认值。小心,因为 SSLContext.getDefault() 返回一个不可修改的类实例(或者更像是默认实例不能重新初始化,但无论如何) ,这就是为什么我们必须使用 SSLContext.getInstance("SSL")。另外,不要忘记将这个新的 SSLContext 设置为默认值,因为没有它,代码就会 poof

    SSLContext sslContext = SSLContext.getInstance("SSL");
    sslContext.init(null, trustManagers, null);
    SSLContext.setDefault(sslContext);
    

    创建您的代理并为其设置身份验证。 (不要使用 System.setProperty(...) 而是使用 Proxy 类。哦,不要被 Type.HTTP 误导。

    Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("<host>", <port>));
    

    为您的代理设置身份验证。 (我使用了不需要身份验证的 a free proxy,所以我现在无法测试这部分问题。

    Authenticator.setDefault(new Authenticator() 
    
      @Override
      protected PasswordAuthentication getPasswordAuthentication() 
        return new PasswordAuthentication("<user>", "<password>".toCharArray());
      
    );
    

    通过将先前创建的代理传递给连接来连接到您的端点。 (我使用了我公司的一个服务 URL,该 URL 要求提供证书——当然,该证书是我在自己的密钥库中导入的。

    URL url = new URL("<endpoint>");
    URLConnection connection = url.openConnection(proxy);
    connection.connect();
    

如果它不能像这样工作(你得到错误)然后尝试使用 HttpsURLConnection

   HttpsURLConnection httpsConnection = (HttpsURLConnection) connection;
   httpsConnection.setAllowUserInteraction(true);
   httpsConnection.setRequestMethod("POST");

基本上,如果服务器(您连接到的 URL 指向的资源所在的位置)要求提供凭据,setAllowUserInteraction 就会启动,对吗?现在,我不能只测试 本身,但我看到你是否可以让这个婴儿使用不需要身份验证来访问其资源的服务器,那么你很高兴去吧,因为服务器会要求你验证自己只有在连接已经建立之后。

如果在这些之后你仍然收到一些错误,那么请发布它。

【讨论】:

感谢您的详细回复。我已经按照你说的做了,但是当我导出并注册请求的证书时,它会显示PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target。如果我还添加 CA 证书(通过导出我通过 IE 找到的证书或通过此处使用的方法以编程方式link),我将再次获得通用Remote host closed connection during handshake 好的。可能会抛出第一个异常,因为证书本身无法验证(它不会导致有效的 CA 或类似的东西)。 Remote host closed ... 这件事可能与您使用的代理不允许您通过它建立安全连接这一事实有关。我写过 Type.HTTP 具有误导性……也许根本不是。 I've found the following related question/answer,看看那个,我想会有帮助的。基本上它说,您无法通过 HTTP 代理访问 HTTPS 资源。 @Slyder 我已经用完了字符,但我想问你这个问题:你能在没有代理的情况下测试连接吗? 我遇到了这个问题,但没有真正跟进,因为如果我将所有内容保持原样,我确实会收到回复,但只需连接到 没有的不同 https url 需要证书。所以我不认为我的问题是代理(除非它以某种方式干扰证书,这会很奇怪,因为我可以通过浏览器或通过我在原始问题中提到的 C# 程序连接到我需要的 URL)编辑 - 我如果我不使用代理,则会出现 Hostname not found 异常 我不认为我的问题是代理(除非它以某种方式干扰证书如果您连接到代理的 HTTP 端口,我认为它可能会导致问题。您为资源建立单个。该行由两个部分组成:you -&gt; proxyproxy -&gt; resource。这两个部分都必须是安全的。我可以连接通过浏览器或通过我提到的 C# 程序访问我需要的 URL 您是否在浏览器和/或 C# 程序中使用代理设置?【参考方案2】:

根据我上次尝试这样做的记忆,您不能使用HttpsURLConnection。您可以查看支持此功能的 Apache HttpClient 库。

这是一个代码示例,给出了该过程的概念:

String server = "example.com";
int port = 443;
EasySSLProtocolSocketFactory psf = new EasySSLProtocolSocketFactory();
InputStream is = readFile("/path/to/certificate");
KeyMaterial km = new KeyMaterial(is, "certpasswd".toCharArray());
easy.setKeyMaterial(km);

Protocol proto = new Protocol("https", (ProtocolSocketFactory) psf, port);
HttpClient httpclient = new HttpClient();
httpclient.getHostConfiguration().setHost(server, port, proto);

编辑(关于汤姆评论):

以下是有关如何获取存储在 Windows 密钥库中的证书的一些想法:

您需要使用 Sun Cryptography 套件(即 Sun Java 6 JDK) 您可以通过以下方式获取 KeyStore:ks = KeyStore.getInstance("Windows-MY"); 您可以这样加载它:ks.load(null, null);。 JVM 将加载 Windos 密钥库并负责询问密钥库密码。 然后您可以像浏览任何其他密钥库一样浏览密钥库。

【讨论】:

错误答案。ks.load(null,null) 不加载 Windows 密钥库。您无法从 java 加载 Windows 密钥库 @user384706:是的,你可以。我邀请您检查我从事的这个项目,该项目加载 Windows 密钥库并允许您访问其中的所有证书:adullact.net/projects/libersign @user384706:哦,这里是关于它的 Sun (Oracle) 文档。以防万一您想查看:java.sun.com/developer/technicalArticles/J2SE/security/#1 ks.load(null,null) 只在内存中初始化并清空 Keystore 对象。它不会加载 Windows 密钥库! KeyStore.getInstance("Windows-MY") -> 此行使用特定的 procider。 Windows-MY providerm 在这种情况下。我之前在第二条评论中给你发了一个链接。【参考方案3】:

原来是私钥问题,因为它被设置为不可导出。因为这意味着我只能从 Windows 商店获取私钥,所以我放弃并“修复”了这个问题,通过大量的混乱来让必要的 jdk6 类正常工作,而不会过多地影响应用程序的其余部分。

【讨论】:

什么样的私钥问题?您谈到了证书(大概是您尝试连接的服务器)。证书包含私钥。使用 SSL,您的对等方会向您发送其 公钥,您可以使用您的受信任的根 CA 公钥副本对其进行身份验证。如果您的对等方的公钥未由根 CA 签名,或者您没有签署它的根 CA 的公钥,那么您需要将其证书导入您的受信任密钥库以信任 它。 SSL 使用密钥来加密批量数据传输,但您不必为那个而烦恼。 当访问位于 Windows 密钥库中的证书时,系统会提示我输入密码,但当我将其导出到 Java 密钥库并在那里访问它时,并没有发生这种情况。我唯一注意到它看起来可能会有所不同的是,当通过 IE 导出证书时,它说“私钥不可导出” 您的证书似乎包含公钥/私钥对。我假设一些 CA 将这样的证书分发给尚未拥有密钥对的人......这很有趣,但也很荒谬,因为 certificate 不应该包含私钥。它应该被提取。我想,一方面有证书的概念,另一方面有一堆半途而废的证书文件格式,几乎可以包含任何东西。这是可悲的。不过感谢您的跟进。

以上是关于如何通过 HttpsURLConnection 使用证书身份验证?的主要内容,如果未能解决你的问题,请参考以下文章

使用客户端证书和 Android 的 HttpsURLConnection 通过 SSL 上传文件

如何让 Jersey 客户端使用 HttpsURLConnection 中设置的 defaultSSLSocketFactory?

Android HttpsURLConnection,如何判断响应是不是被缓存?

使用HttpsURLConnection时如何覆盖Android发送给服务器的密码列表?

httpsUrlConnection 如何设置的默认sslcontext和 hostnameverifier?

httpsUrlConnection 如何设置的默认sslcontext和 hostnameverifier?