我的项目是服务器与服务器之间调接口,调用是需要ssl证书双向认证的。
yml中配置:
## 证书双向认证配置(本系统作为客户端)
client:
ssl:
abs:
# jks与pkcs12(即pfx)都可以。type不区分大小写
# path: ssl/abs/abs@aaa.abc.com.jks
# type: JKS
path: ssl/abs/abs@aaa.abc.com.pfx
type: PKCS12
password: 123456
# ssl双向认证开关
auth: true
config/RestTemplateConfig.java中:
package com.abc.config;
import lombok.Data;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.cert.CertificateException;
@Data
@Configuration
@ConfigurationProperties(prefix="client.ssl.abs")
public class RestTemplateConfig {
private String path;
private String type;
private String password;
private boolean auth;
@Bean
public RestTemplate getRestTemplate() throws CertificateException, UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException, IOException {
// https使用
RestTemplate restTemplate = new RestTemplate(getHttpRequestFactory());
// http使用
// RestTemplate restTemplate = new RestTemplateBuilder().setConnectTimeout(Duration.ofSeconds(1000)).setReadTimeout(Duration.ofSeconds(5000)).build();
//TODO 还是未解决乱码
//解决中文乱码方式一
restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
//解决中文乱码方式二
//restTemplate.getMessageConverters().set(1,new MappingJackson2HttpMessageConverter());
//StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter(StandardCharsets.UTF_8);
//stringHttpMessageConverter.setWriteAcceptCharset(true);
return restTemplate;
}
public HttpComponentsClientHttpRequestFactory getHttpRequestFactory() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, KeyManagementException, UnrecoverableKeyException {
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
requestFactory.setConnectTimeout(1000);
requestFactory.setReadTimeout(5000);
if(auth) {
requestFactory.setHttpClient(getHttpClient());
}
return requestFactory;
}
/**
*
* 开发环境中,不一定会有域名,因此可能会造成证书域名和真实服务器IP无法匹配而校验失败,提供两种解决方案:
*
* 方式一(不验证服务端证书):
* 因此在开发环境中,客户端需要加上这么一段配置重写的verify方法,用来跳过服务端证书校验。
* 代码:
* TrustStrategy acceptingTrustStrategy = (X509Certificate[] x509Certificates, String s) -> true;
* //配置信任链
* SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy);
*
* 方式二(推荐)(仅不验证服务端的证书与域名匹配,其它服务端证书项正常校验):
* 在SSLConnectionSocketFactory中加上new NoopHostnameVerifier()跳过验证。
* (
* 说明:但是若采取方式二,就需要预先使用keytool工具导入证书到'jdk1.8.0_231/jre/lib/security/cacerts'中
* 注意:有证书链要导入整个证书链
* 命令(在jdk的security下执行,cacerts通用默认密码是changeit):keytool -import -keystore cacerts -alias CA_SUB -file ca_root_and_sub.crt
* )
*
*/
public CloseableHttpClient getHttpClient() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, KeyManagementException, UnrecoverableKeyException {
char[] password = this.password.toCharArray();
KeyStore keyStore = KeyStore.getInstance(type);
InputStream in = this.getClass().getClassLoader().getResourceAsStream(path);
// 加载classpath下的文件要使用getClass().getClassLoader().getResourceAsStream(***),而不能用getClass().getResourceAsStream(***),获取不到
// keyStore.load(new FileInputStream(new File(path)), password);
keyStore.load(in, password);
// TrustStrategy acceptingTrustStrategy = (X509Certificate[] x509Certificates, String s) -> true;
SSLContext sslContext = org.apache.http.ssl.SSLContexts.custom()
// 配置信任链,用于跳过验证服务器证书
//.loadTrustMaterial(null, acceptingTrustStrategy)
.loadKeyMaterial(keyStore, password)
.build();
// 单纯不校验域名证书匹配(NoopHostnameVerifier)
SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext, new NoopHostnameVerifier());
// 校验域名证书匹配
// SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext);
// 或
// SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(
// sslContext,
// // new String[]{"TLSv1","TLSv1.1","TLSv1.2"},
// // new String[]{"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384"},
// SSLConnectionSocketFactory.getDefaultHostnameVerifier());
return HttpClients.custom().setSSLSocketFactory(csf).build();
}
}
重点在于其中的getHttpClient()方法:
开发环境中,不一定会有域名,因此可能会造成证书域名和真实服务器IP无法匹配而校验失败,提供两种解决方案:
方式一(不验证服务端证书):
在开发环境中,客户端需要加上这么一段配置重写的verify方法,用来跳过服务端证书校验。
代码:
TrustStrategy acceptingTrustStrategy = (X509Certificate[] x509Certificates, String s) -> true;
//配置信任链
SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy);
(推荐)(仅不验证服务端的证书与域名匹配,其它服务端证书项正常校验):
在SSLConnectionSocketFactory中加上new NoopHostnameVerifier()跳过验证。
说明:
但是若采取方式二,就需要预先使用keytool工具导入证书到“jdk1.8.0_231/jre/lib/security/cacerts”中注意:
有证书链要导入整个证书链
命令(在jdk的security下执行,cacerts通用默认密码是changeit):
① 先进入$JAVA_HOME/jre/lib/security/文件夹中
② keytool -import -keystore cacerts -alias CA_SUB -file ca_root_and_sub.crt
或linux中直接执行如下命令:
keytool -import -keystore $JAVA_HOME/jre/lib/security/cacerts -alias CA_SUB -file ca_root_and_sub.crt