总述
https简单来说就是在http协议的基础上增加了一层安全协议。通常为TLS或者SSL(一般现在都采用TLS,更加安全)。这一层安全协议的最主要的作用有两个:
1. 验证服务端或客户端的合法性
2. 商量出最终用来http通信的对称加密秘钥
本次仅仅讲第1点
单向认证与双向认证
所谓的认证既确认对方身份,单向认证一般是指客户端确认服务端身份,双向认证则是指在客户端需要确认服务端身份的同时,服务端也需要确认客户端的身份。
具体可以通过下面两幅图来看下区别:
单向认证
双向认证
show me the code
这里给出在使用httpClient的时候如何初始化连接池。
public class HttpClientInitConfig {
/**
* ssl双向认证证书 默认客户端不验证服务端返回
*/
private TrustStrategy trustStrategy = TrustAllStrategy.INSTANCE;
private String sslProtocol = "TLSV1.2";
/**
* ssl双向认证客户端的keystore
*/
private String keyStorePath;
/**
* ssl双向认证客户端keystore的秘钥
*/
private String storePwd;
/**
* ssl双向认证客户端私钥证书密码
*/
private String keyPwd;
/**
* 秘钥库证书类型
*/
private String keyStoreType = "PKCS12";
public String getKeyStoreType() {
return keyStoreType;
}
public void setKeyStoreType(String keyStoreType) {
this.keyStoreType = keyStoreType;
}
private int maxPerRoute = 200;
private int maxTotal = 200;
private int validateAfterInactivity = 60000;
public int getValidateAfterInactivity() {
return validateAfterInactivity;
}
public void setValidateAfterInactivity(int validateAfterInactivity) {
this.validateAfterInactivity = validateAfterInactivity;
}
public TrustStrategy getTrustStrategy() {
return trustStrategy;
}
public void setTrustStrategy(TrustStrategy trustStrategy) {
this.trustStrategy = trustStrategy;
}
public String getSslProtocol() {
return sslProtocol;
}
public void setSslProtocol(String sslProtocol) {
this.sslProtocol = sslProtocol;
}
public String getKeyStorePath() {
return keyStorePath;
}
public void setKeyStorePath(String keyStorePath) {
this.keyStorePath = keyStorePath;
}
public String getStorePwd() {
return storePwd;
}
public void setStorePwd(String storePwd) {
this.storePwd = storePwd;
}
public String getKeyPwd() {
return keyPwd;
}
public void setKeyPwd(String keyPwd) {
this.keyPwd = keyPwd;
}
public int getMaxPerRoute() {
return maxPerRoute;
}
public void setMaxPerRoute(int maxPerRoute) {
this.maxPerRoute = maxPerRoute;
}
public int getMaxTotal() {
return maxTotal;
}
public void setMaxTotal(int maxTotal) {
this.maxTotal = maxTotal;
}
}
public class CacheablePooledHttpClient {
private static final Logger LOG = LoggerFactory.getLogger(CacheablePooledHttpClient.class);
private Map<String, HttpClient> httpClientMap = new ConcurrentHashMap<>();
private Map<String, HttpClientConnectionManager> connectionManagerMap = new ConcurrentHashMap<>();
CacheablePooledHttpClient() {
}
private static final class InstanceHolder {
static final CacheablePooledHttpClient instance = new CacheablePooledHttpClient();
}
public static CacheablePooledHttpClient getInstance() {
return InstanceHolder.instance;
}
public HttpClient getHttpClient(String clientKey) {
return this.httpClientMap.get(clientKey);
}
public HttpClient initHttpClient(String clientKey, HttpClientInitConfig initConfig) {
if (this.httpClientMap.containsKey(clientKey)) {
return this.httpClientMap.get(clientKey);
}
synchronized (httpClientMap) {
if (this.httpClientMap.containsKey(clientKey)) {
return this.httpClientMap.get(clientKey);
}
try {
SSLContextBuilder sslContextBuilder = SSLContexts.custom().setProtocol(initConfig.getSslProtocol()).loadTrustMaterial(initConfig.getTrustStrategy());
// ssl双向认证的时候,客户端需要加载的证书
if (StringUtils.isNotBlank(initConfig.getKeyStorePath()) && StringUtils.isNotBlank(initConfig.getStorePwd())) {
final KeyStore ks = CryptUtils.loadKeyStore(initConfig.getKeyStorePath(), initConfig.getStorePwd().toCharArray(), initConfig.getKeyStoreType());
sslContextBuilder.loadKeyMaterial(ks, initConfig.getKeyPwd().toCharArray(), (a, b) -> {
try {
return CryptUtils.getFirstAliase(ks);
} catch (KeyStoreException e) {
throw new HttpClientInitException(e);
}
});
LOG.info("使用客户端证书【{}】初始化http连接池", initConfig.getKeyStorePath());
}
SSLContext sslContext = sslContextBuilder.build();
Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory> create()
.register("http", PlainConnectionSocketFactory.getSocketFactory())
.register("https", new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE)).build();
@SuppressWarnings("resource")
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry);
connectionManager.setValidateAfterInactivity(initConfig.getValidateAfterInactivity()); // 半连接状态关闭时间
connectionManager.setDefaultMaxPerRoute(initConfig.getMaxPerRoute());
connectionManager.setMaxTotal(initConfig.getMaxTotal());
connectionManagerMap.put(clientKey, connectionManager);
httpClientMap.put(clientKey, new HttpClient(initHttpClient(connectionManager)));
LOG.info("初始化key为【{}】的httpClient连接池成功!", clientKey);
} catch (Exception ex) {
throw new HttpClientInitException(ex);
}
try {
Runtime.getRuntime().addShutdownHook(new ContainerShutdownHook(this));
} catch (Exception e) {
LOG.error("添加关闭钩子异常!", e);
}
}
return httpClientMap.get(clientKey);
}
private CloseableHttpClient initHttpClient(PoolingHttpClientConnectionManager connectionManager) {
RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(5000).setSocketTimeout(300000).setConnectionRequestTimeout(30000)
.build();
return HttpClients.custom().setDefaultRequestConfig(requestConfig).setConnectionManager(connectionManager)
.setConnectionManagerShared(false).setRetryHandler(new DefaultHttpRequestRetryHandler(0, false)).evictExpiredConnections()
.evictIdleConnections(1800L, TimeUnit.SECONDS).build();
}
public void close() {
try {
for (HttpClient client : this.httpClientMap.values()) {
if (client != null) {
client.getCloseableHttpClient().close();
}
}
for (HttpClientConnectionManager connectionManager : this.connectionManagerMap.values()) {
if (connectionManager != null) {
connectionManager.shutdown();
}
}
LOG.info("Close httpClient completed");
} catch (Exception e) {
LOG.error("shutdown httpcliet exception: ", e);
}
}
}
这里其实本来是双向认证的,但是因为时间原因所以偷了个懒,略过了客户端对服务器端证书的校验,而直接使用`TrustAllStrategy.INSTANCE`。其实如果客户端需要对服务器端证书进行校验的话可以参考如下代码设置`trustStrategy`:
KeyStore trustKeyStore = KeyStore.getInstance("jks");
trustKeyStore.load(new FileInputStream("D:\trustkeystore.jks"), "123456".toCharArray());
sslContextBuilder.loadTrustMaterial(trustKeyStore, new TrustSelfSignedStrategy());
小结
- 证书是存在证书链的,根证书能对所有子证书进行验证,在进行双向认证的时候服务端和客户端需要初始化的证书都是从根证书生成的
- 在TLS协议过程中发送的客户端和服务端证书(.crt)其实都是公钥证书,外加一些版本号、身份、签名等信息
- 客户端可以通过使用
TrustAllStrategy
来忽略对服务器证书中的身份校验,而仅仅是去拿到证书里面的公钥
- 如果服务端对客户端证书有校验,而客户端在使用
HttpClient
请求的时候未loadKeyMaterial
发送客户端证书,则会报类似如下错误:
javax.net.ssl.SSLHandshakeException: Remote host closed connection during handshake
Caused by: javax.net.ssl.SSLHandshakeException: Remote host closed connection during handshake
at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1367) ~[?:1.8.0_192]
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1395) ~[?:1.8.0_192]
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1379) ~[?:1.8.0_192]
at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1367) ~[?:1.8.0_192]
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1395) ~[?:1.8.0_192]
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1379) ~[?:1.8.0_192]
- 如果客户端未使用
TrustAllStrategy
初始化HttpClient
且指定对服务端的域名校验器不是NoopHostnameVerifier.INSTANCE
, 那么如果服务端生成证书时指定的域名/ip不是服务端实际域名/ip。那么会报类似如下错误:
Certificate for <xxxx> doesn't match any of the subject alternative name