如何在Apache HttpClient中使用SSL客户端证书?

41

在Python中我像这样使用requests:

requests.put(
              webdavURL, 
              auth=(tUsername, tPassword), 
              data=webdavFpb, 
              verify=False, 
              cert=("/path/to/file.pem", "/path/to/file.key"))

易如反掌。

现在我需要使用Apache HttpClient在Java中实现相同的功能。在使用HttpClient进行请求时,如何传递客户端证书?

3个回答

60
我认为主要的区别在于Java中,您通常将密钥和证书放入密钥库中,并从那里使用它。就像您经常提到的人们想要使用一个单独的库,例如httpcomponents client(就像您在Python示例中使用requests library一样),这样可以方便地使用。以下是使用先前提到的库从密钥库中使用客户端证书的示例:
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;
import org.junit.Test;

import javax.net.ssl.SSLContext;
import java.io.InputStream;
import java.security.KeyStore;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

public class MyClientCertTest {

    private static final String KEYSTOREPATH = "/clientkeystore.jks"; // or .p12
    private static final String KEYSTOREPASS = "keystorepass";
    private static final String KEYPASS = "keypass";

    KeyStore readStore() throws Exception {
        try (InputStream keyStoreStream = this.getClass().getResourceAsStream(KEYSTOREPATH)) {
            KeyStore keyStore = KeyStore.getInstance("JKS"); // or "PKCS12"
            keyStore.load(keyStoreStream, KEYSTOREPASS.toCharArray());
            return keyStore;
        }
    }
    @Test
    public void readKeyStore() throws Exception {
        assertNotNull(readStore());
    }
    @Test
    public void performClientRequest() throws Exception {
        SSLContext sslContext = SSLContexts.custom()
                .loadKeyMaterial(readStore(), KEYPASS.toCharArray()) // use null as second param if you don't have a separate key password
                .build();

        HttpClient httpClient = HttpClients.custom().setSSLContext(sslContext).build();
        HttpResponse response = httpClient.execute(new HttpGet("https://slsh.iki.fi/client-certificate/protected/"));
        assertEquals(200, response.getStatusLine().getStatusCode());
        HttpEntity entity = response.getEntity();

        System.out.println("----------------------------------------");
        System.out.println(response.getStatusLine());
        EntityUtils.consume(entity);
    }
}

用于依赖版本的Maven pom:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.acme</groupId>
    <artifactId>httptests</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.3</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.9</version>
                <!-- this is not needed, but useful if you want to debug what's going
                     on with your connection -->
                <configuration>
                    <argLine>-Djavax.net.debug=all</argLine>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

我还发布了一个简单的测试页面,用于测试客户端证书。


为了展示它的可行性,以下是一个使用标准Java API而不需要额外库来使用客户端证书的例子。

import org.junit.Test;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.KeyStore;

public class PlainJavaHTTPS2Test {

    @Test
    public void testJKSKeyStore() throws Exception {
        final String KEYSTOREPATH = "clientkeystore.jks";
        final char[] KEYSTOREPASS = "keystorepass".toCharArray();
        final char[] KEYPASS = "keypass".toCharArray();

        try (InputStream storeStream = this.getClass().getResourceAsStream(KEYSTOREPATH)) {
            setSSLFactories(storeStream, "JKS", KEYSTOREPASS, KEYPASS);
        }
        testPlainJavaHTTPS();
    }
    @Test
    public void testP12KeyStore() throws Exception {
        final String KEYSTOREPATH = "clientkeystore.p12";
        final char[] KEYSTOREPASS = "keystorepass".toCharArray();
        final char[] KEYPASS = "keypass".toCharArray();

        try (InputStream storeStream = this.getClass().getResourceAsStream(KEYSTOREPATH)) {
            setSSLFactories(storeStream, "PKCS12", KEYSTOREPASS, KEYPASS);
        }
        testPlainJavaHTTPS();
    }
    private static void setSSLFactories(InputStream keyStream, String keystoreType, char[] keyStorePassword, char[] keyPassword) throws Exception
    {
        KeyStore keyStore = KeyStore.getInstance(keystoreType);

        keyStore.load(keyStream, keyStorePassword);

        KeyManagerFactory keyFactory =
                KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());

        keyFactory.init(keyStore, keyPassword);

        KeyManager[] keyManagers = keyFactory.getKeyManagers();

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

    public void testPlainJavaHTTPS() throws Exception {
        String httpsURL = "https://slsh.iki.fi/client-certificate/protected/";
        URL myUrl = new URL(httpsURL);
        HttpsURLConnection conn = (HttpsURLConnection)myUrl.openConnection();
        try (InputStream is = conn.getInputStream()) {
            InputStreamReader isr = new InputStreamReader(is);
            BufferedReader br = new BufferedReader(isr);

            String inputLine;

            while ((inputLine = br.readLine()) != null) {
                System.out.println(inputLine);
            }
        }
    }
}

这是第三个版本的代码,代码量相对较少,但需要满足两个条件:a) keystore是磁盘上的文件而不在jar包中;b) 密钥密码必须与keystore密码相同。

import org.junit.BeforeClass;
import org.junit.Test;

import java.net.URL;
import java.io.*;
import javax.net.ssl.HttpsURLConnection;

public class PlainJavaHTTPSTest {

    @BeforeClass
    public static void setUp() {
        System.setProperty("javax.net.ssl.keyStore", "/full/path/to/clientkeystore-samepassword.jks");
        System.setProperty("javax.net.ssl.keyStorePassword", "keystorepass");
    }

    @Test
    public void testPlainJavaHTTPS() throws Exception {
        String httpsURL = "https://slsh.iki.fi/client-certificate/protected/";
        URL myUrl = new URL(httpsURL);
        HttpsURLConnection conn = (HttpsURLConnection)myUrl.openConnection();
        try (InputStream is = conn.getInputStream()) {
            InputStreamReader isr = new InputStreamReader(is);
            BufferedReader br = new BufferedReader(isr);

            String inputLine;

            while ((inputLine = br.readLine()) != null) {
                System.out.println(inputLine);
            }
        }
    }
}

上面在代码中设置的属性当然也可以作为启动参数,-Djavax.net.ssl.keyStore=/full/path/to/clientkeystore-samepassword.jks-Djavax.net.ssl.keyStorePassword=keystorepass


1
如果密钥库包含多个密钥,代码如何知道使用哪个密钥? - David Brossard
1
@DavidBrossard 当存在多个匹配的证书时,SSL客户端证书是如何选择的? - eis
我已经尝试了几天来使证书代码有效,我已经以多种方式尝试了该代码但仍然没有效果。我没有jks或p12文件,只有/C/Program Files/Java/jdk1.8.0_271/jre/lib/security/cacerts文件,我遇到了“PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target”的错误。我尝试了许多SO帖子 - https://dev59.com/xeo6XIcBkEYKwwoYLhbh;这仅适用于.jks/.p12文件吗?我该如何获得这些文件?请帮帮我。 - ASheppardWork
@ASheppardWork 听起来你没有客户端证书,如果你只有 cacerts 文件,那么它与这个问题或这段代码无关? - eis

10
如果您想使用Apache HTTP客户端而不是Java HTTP客户端,则必须向SSLFactory提供您的密钥库,并配置DefaultHTTPClient在HTTPS协议中使用它。您可以在这里找到一个可行的示例。希望能对您有所帮助。

你能给一些使用Java HTTP客户端的指针吗?我在使用Apache的时候遇到了困难。 - Ravi
8
我可能错了,但那个例子似乎只涉及使用自定义的信任存储库(例如信任一个通常不被信任的证书),而不是提供客户端证书来进行客户端认证。 - Matt Sheppard
1
这并没有回答问题。这只会添加一个自定义的信任存储,而不是客户端证书。 - sstendal
2
这个例子是关于自定义信任存储的。它并不展示如何提供客户端证书。 - Kanishka Dilshan

0
使用 Apache HttpClient 5.2 的流畅 API,代码如下:
System.setProperty("javax.net.ssl.keyStore", "/full/path/to/clientkeystore-samepassword.p12");
System.setProperty("javax.net.ssl.keyStorePassword", "password");
System.setProperty("javax.net.ssl.keyStoreType", "PKCS12");
System.setProperty("javax.net.debug", "ssl"); // very verbose debug

String url = "https://example.org/client-certificate/protected/";
        
Content content = Request.get(url)
  .execute()
  .returnContent();
System.out.println(content);

你好 - 你能提供更多信息吗?我们如何实际构建一个能够进行双向通信的客户端呢?谢谢! - user555303

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接