起因
某日部分请求报"unable to find valid certification path to requested target",从异常堆栈来看是代码里设置的没生效,还是去做证书校验,证书校验失败导致最终抛异常。
image.png
分析
代码里确实在创建sslContext的时候指定了自定义的X509TrustManager,并信任服务端证书,为啥没有生效呢?
从下面的代码看到,在获取新的httpClient实例之前,会调用 Protocol.registerProtocol注册协议和sslContext的关联关系。
public SSLContext createTrustSSLContext(CommunicationConfig config) throws GeneralSecurityException, IOException {
String protocol = StringUtils.defaultIfBlank(ConfigUtil.getSSLProtocol(config), PROTOCOL);
SSLContext sslContext = SSLContext.getInstance(protocol);
TrustManager[] tm = { getForeverTrusterManager() };
sslContext.init(null, tm, new SecureRandom());
return sslContext;
}
private TrustManager getForeverTrusterManager() {
return new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] input, String authType) throws CertificateException {
// 建立这种永远信任的连接,不需要校验
}
@Override
public void checkServerTrusted(X509Certificate[] input, String arg1) throws CertificateException {
// 建立这种永远信任的连接,不需要校验
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[] {};
}
};
}
private HttpClient getAvailableClient(CommunicationConfig config) {
try {
String key = config.getKey();
HttpClient client = instanceMap.get(key);
if (client == null) {
synchronized (instanceMap) {
if ((client = instanceMap.get(key)) == null) {
// 注册https协议
registerSSLProtocol(config);
// 创建客户端
client = contructClient(config);
// 注册协议处理类
ClientProtocolHandler handler = ProtocolFactory.getClientProtocolHandler(config.getProtocol()).newInstance();
handler.setConfig(config);
handlerMap.put(key, handler);
instanceMap.put(key, client);
}
}
}
return client;
} catch (Exception e) {
errorLogger.error("HTTP客户端注册异常{}", e);
throw new YtgwException(CommunicationErrorCode.NO_CLIENT_FOUND, e);
}
}
private void registerSSLProtocol(CommunicationConfig config) {
if (config.getProtocol() == TransportProtocol.HTTPS) {
// 已经注册过就不用再注册了,如果有自定义协议用 HTTPS_SCHEMA
String scheme = ConfigUtil.getHttpsSchema(config);
TransportURL url = config.getUri();
SSLContext context = SSLProtocolHelper.create(config);
SSLSocketFactory socketFactory = context.getSocketFactory();
ProtocolSocketFactory factory = new SSLSocketFactoryImpl(socketFactory, config);
// 注册的协议只能使用全小写
Protocol myHttps = new Protocol(scheme, factory, url.getPort());
Protocol.registerProtocol(scheme, myHttps);
logger.info("注册HTTPS协议,scheme={},url={},port={}", scheme, url.getUrl(), url.getPort());
}
}
从下面的代码能看到在打开连接的时候,会从protocol对象获取socketFactory,这个socketFactory就是上面注册上去的。
image.png
如果没有注册对应的协议,protocol获取到的socketFactory又是啥呢?
从下面的代码能看到protocol会调用SSLProtocolSocketFactory.getSocketFactory()创建一个默认的socketFactory,这个默认的socketFactory就是使用最上面异常堆栈里的sun.security.ssl.X509TrustManagerImpl去做证书的校验,如果证书有问题,就会出现校验失败的情况。
到这里就知道产生问题的根本原因了,如果Protocol.registerProtocol时注册的是协议A,访问的是协议B,protocol就会创建默认的socketFactory去做证书校验,最终出现证书校验失败的问题。
public static Protocol getProtocol(String id)
throws IllegalStateException {
if (id == null) {
throw new IllegalArgumentException("id is null");
}
Protocol protocol = (Protocol) PROTOCOLS.get(id);
if (protocol == null) {
protocol = lazyRegisterProtocol(id);
}
return protocol;
}
private static Protocol lazyRegisterProtocol(String id)
throws IllegalStateException {
if ("http".equals(id)) {
final Protocol http
= new Protocol("http", DefaultProtocolSocketFactory.getSocketFactory(), 80);
Protocol.registerProtocol("http", http);
return http;
}
if ("https".equals(id)) {
final Protocol https
= new Protocol("https", SSLProtocolSocketFactory.getSocketFactory(), 443);
Protocol.registerProtocol("https", https);
return https;
}
throw new IllegalStateException("unsupported protocol: '" + id + "'");
}