前言
有些HTTP 服务要求客户端在请求的时候使用证书加密/解密/授权 (客户端证书授权)。比如说下面的 curl 命令
curl --insecure \
--cert cert.crt \
--key key.key \
-d '{"name":"joel"}' \
-H "Content-Type: application/json" \
-X POST https://localhost:13321/get_user_info
那么,在Java中如何实现呢?
搜索关键词
- Java HTTPS client certificate authentication
- PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
- Certificate for <xxx.xxx.xxx.xxx> doesn’t match any of the subject alternative names: [xxx.xxx]
- Chia RPC Java
证书转换
将 crt 和 key 导出为 p12
openssl pkcs12 -export -in cert.crt -inkey key.key -out cert.p12 -name "joelzho.com"
备注:
- 将输出的密码记住
-name
的值最好是访问该服务接口的域名
示例代码
示例代码环境
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.12</version>
</dependency>
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.TrustAllStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.SSLContexts;
import org.junit.Test;
import javax.net.ssl.SSLContext;
import java.io.FileInputStream;
import java.security.KeyStore;
/**
* @author Joel
*/
public class TestClientCertAuthHtttp {
@Test
public void test() throws Exception {
final String certFile = "/path/to/cert.p12";
final String storePass = "store-pass";
final String keyPass = "key-pass";
KeyStore keyStore;
try (FileInputStream fis = new FileInputStream(certFile)) {
keyStore = KeyStore.getInstance("PKCS12"); // or "JKS"
keyStore.load(fis, storePass.toCharArray());
}
SSLContextBuilder sslContextBuilder = SSLContexts.custom();
sslContextBuilder.loadTrustMaterial(TrustAllStrategy.INSTANCE);
sslContextBuilder.loadKeyMaterial(keyStore, keyPass.toCharArray());
SSLContext sslContext = sslContextBuilder.build();
HttpClientBuilder httpClientBuilder = HttpClients.custom();
httpClientBuilder.setSSLContext(sslContext);
httpClientBuilder.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE);
CloseableHttpClient httpClient = httpClientBuilder.build();
// ....
}
}
代码解读
密码
p12 (PKCS12) 证书只有一个密码,即 storePass
和 keyPass
相等.
如果是 jks 证书的话,两个密码大概率不同(看你导出jks证书的时候怎么设置的)
loadTrustMaterial
sslContextBuilder.loadTrustMaterial(TrustAllStrategy.INSTANCE);
这句代码主要是用来解决自签名证书不被Java信任的问题。相关的类有
- TrustStrategy: 一个接口
- TrustAllStrategy: 信任所有
- TrustSelfSignedStrategy: 信任自签名证书
这里选择 TrustAllStrategy
的原因是 TrustSelfSignedStrategy
的实现在开发环境可能无效(比如说不小心将证书导入到系统中了,然后你代码中又指定了一遍,详情看该类的实现代码)。
当然,最安全的做法是自己实现 TrustStrategy
在里面写验证。
相关异常:PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
setSSLHostnameVerifier
httpClientBuilder.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE);
这句代码主要是用来忽略证书里面的 host
和 实际请求的 host
不一致验证。
也许证书颁发机构给的证书里面写定的是一个域名,但是你本机做测试的时候没有域名或这个证书被用在不同的服务里面。那么加上这个句代码即可。
相关异常:Certificate for <xxx.xxx.xxx.xxx> doesn’t match any of the subject alternative names: [xxx.xxx]
番外
jks 证书怎么导出?
在这个示例中,你要先有一个 p12 格式的证书 (见上文)
keytool -importkeystore \
-deststorepass storepassword \
-destkeypass keypassword \
-destkeystore cert.jks \
-srckeystore cert.p12 \
-srcstoretype PKCS12 \
-srcstorepass srcstorepass \
-alias joelzho.com
备注:
deststorepass
和destkeypass
的值要用到代码中srcstorepass
是 p12证书的密码-alias
的值最好是访问该服务接口的域名- 获取 KeyStore实例的代码修改为:
KeyStore.getInstance("JKS");