起因
某日部分請(qǐng)求報(bào)"unable to find valid certification path to requested target"姻政,從異常堆棧來(lái)看是代碼里設(shè)置的沒(méi)生效浩考,還是去做證書(shū)校驗(yàn)廊移,證書(shū)校驗(yàn)失敗導(dǎo)致最終拋異常扭粱。
分析
代碼里確實(shí)在創(chuàng)建sslContext的時(shí)候指定了自定義的X509TrustManager舵鳞,并信任服務(wù)端證書(shū),為啥沒(méi)有生效呢琢蛤?
從下面的代碼看到,在獲取新的httpClient實(shí)例之前抛虏,會(huì)調(diào)用 Protocol.registerProtocol注冊(cè)協(xié)議和sslContext的關(guān)聯(lián)關(guān)系博其。
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 {
// 建立這種永遠(yuǎn)信任的連接,不需要校驗(yàn)
}
@Override
public void checkServerTrusted(X509Certificate[] input, String arg1) throws CertificateException {
// 建立這種永遠(yuǎn)信任的連接迂猴,不需要校驗(yàn)
}
@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) {
// 注冊(cè)https協(xié)議
registerSSLProtocol(config);
// 創(chuàng)建客戶端
client = contructClient(config);
// 注冊(cè)協(xié)議處理類(lèi)
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客戶端注冊(cè)異常{}", e);
throw new YtgwException(CommunicationErrorCode.NO_CLIENT_FOUND, e);
}
}
private void registerSSLProtocol(CommunicationConfig config) {
if (config.getProtocol() == TransportProtocol.HTTPS) {
// 已經(jīng)注冊(cè)過(guò)就不用再注冊(cè)了,如果有自定義協(xié)議用 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);
// 注冊(cè)的協(xié)議只能使用全小寫(xiě)
Protocol myHttps = new Protocol(scheme, factory, url.getPort());
Protocol.registerProtocol(scheme, myHttps);
logger.info("注冊(cè)HTTPS協(xié)議,scheme={},url={},port={}", scheme, url.getUrl(), url.getPort());
}
}
從下面的代碼能看到在打開(kāi)連接的時(shí)候慕淡,會(huì)從protocol對(duì)象獲取socketFactory,這個(gè)socketFactory就是上面注冊(cè)上去的沸毁。
如果沒(méi)有注冊(cè)對(duì)應(yīng)的協(xié)議峰髓,protocol獲取到的socketFactory又是啥呢?
從下面的代碼能看到protocol會(huì)調(diào)用SSLProtocolSocketFactory.getSocketFactory()創(chuàng)建一個(gè)默認(rèn)的socketFactory息尺,這個(gè)默認(rèn)的socketFactory就是使用最上面異常堆棧里的sun.security.ssl.X509TrustManagerImpl去做證書(shū)的校驗(yàn)携兵,如果證書(shū)有問(wèn)題,就會(huì)出現(xiàn)校驗(yàn)失敗的情況搂誉。
到這里就知道產(chǎn)生問(wèn)題的根本原因了徐紧,如果Protocol.registerProtocol時(shí)注冊(cè)的是協(xié)議A,訪問(wèn)的是協(xié)議B,protocol就會(huì)創(chuàng)建默認(rèn)的socketFactory去做證書(shū)校驗(yàn)并级,最終出現(xiàn)證書(shū)校驗(yàn)失敗的問(wèn)題拂檩。
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 + "'");
}