SSL错误解决方案
1.问题报告
前两天在客户现场环境的同学,在进行跟第三方业务联调的过程中出现了一个https调用的错误,具体报错信息为javax.net.ssl.SSLException:Unrecognized SSL message,plaintext connection?
2.问题分析
这个错误意思是说,无法识别 SSL 信息,明文连接?看这个意思是说在使用 https 协议访问网络资源时无法识别 SSL 信息。但是在我们将程序发布客户现场前已经充分测试通过的,为什么在客户现场就会出现这个错误呢?
SSL(Secure Socket Layer 安全套接层)是基于HTTPS下的一个协议加密层,最初是由网景公司(Netscape)研发,后被IETF(The Internet Engineering Task Force - 互联网工程任务组)标准化后写入(RFCRequest For Comments 请求注释),RFC里包含了很多互联网技术的规范! 起初是因为HTTP在传输数据时使用的是明文(虽然说POST提交的数据时放在报体里看不到的,但是还是可以通过抓包工具窃取到)是不安全的,为了解决这一隐患网景公司推出了SSL安全套接字协议层,SSL是基于HTTP之下TCP之上的一个协议层,是基于HTTP标准并对TCP传输数据时进行加密,所以HPPTS是HTTP+SSL/TCP的简称。 SSL协议位于TCP/IP协议与各种应用层协议之间,为数据通讯提供安全支持。SSL协议可分为两层: SSL记录协议(SSL Record Protocol):它建立在可靠的传输协议(如TCP)之上,为高层协议提供数据封装、压缩、加密等基本功能的支持。 SSL握手协议(SSL Handshake Protocol):它建立在SSL记录协议之上,用于在实际的数据传输开始前,通讯双方进行身份认证、协商加密算法、交换加密密钥等。
3.初步方案
本来 https 是在 http 的基础上进行加密。使用 SSL 协议进行加密。这样通讯的双方在通讯前就要去做身份校验,通过证书的方式验证身份。原来是证书方面的问题,需要我们调整一下代码,使其信任所有证书(忽略证书的校验),根据不同的HTTP请求工具会有所不同,我们这里用的是HttpClient
,不同的版本的写法有所不同,不过整体思路是一致的,就是信任所有证书
Ø HttpClient <4.3版本
现在让我们将HTTPClient配置为信任所有证书,无论其有效性如何
@Test public final void test() throws GeneralSecurityException { String urlOverHttps = "https://www.baidu.com/"; HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(); CloseableHttpClient httpClient = (CloseableHttpClient) requestFactory.getHttpClient(); //信用所有证书 TrustStrategy acceptingTrustStrategy = (cert, authType) -> true; //NoopHostnameVerifier类: 作为主机名验证工具,实质上关闭了主机名验证,它接受任何 //有效的SSL会话并匹配到目标主机 SSLSocketFactory sf = new SSLSocketFactory(acceptingTrustStrategy, ALLOW_ALL_HOSTNAME_VERIFIER); httpClient.getConnectionManager().getSchemeRegistry().register(new Scheme("https", 8443, sf)); ResponseEntity<String> response = new RestTemplate(requestFactory). exchange(urlOverHttps, HttpMethod.GET, null, String.class); assertThat(response.getStatusCode().value(), equalTo(200)); }
Ø HttpClient 4.4及更高版本
使用新的HTTPClient,现在我们有了一个增强的,重新设计的默认SSL主机名验证程序。此外,通过引入SSLConnectionSocketFactory和RegistryBuilder,可以轻松构建
SSLSocketFactory
@Test public final void test() throws GeneralSecurityException { String urlOverHttps = "https://www.baidu.com/"; //信用所有证书 TrustStrategy acceptingTrustStrategy = (cert, authType) -> true; SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build(); SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE); Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory> create() .register("https", sslsf) .register("http", new PlainConnectionSocketFactory()) .build(); BasicHttpClientConnectionManager connectionManager = new BasicHttpClientConnectionManager(socketFactoryRegistry); CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(sslsf) .setConnectionManager(connectionManager).build(); HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient); ResponseEntity<String> response = new RestTemplate(requestFactory) .exchange(urlOverHttps, HttpMethod.GET, null, String.class); assertThat(response.getStatusCode().value(), equalTo(200)); }
4.初次回归
根据对应的解决方案,我们同步调整对程序使用HttpClient封装的HTTP请求工具类进行类调整,然后重新发布客户现回归测试发现仍然存在同样的SSL错误,怎么会这样呢?
/** * @description 连接池配置 * 采用PoolingHttpClientConnectionManager 可以为多线程提供并发HTTP请求服务。主要作用就是分配连接、 * 回收连接等..当请求一个新的连接时,如果连接池有有可用的持久连接,连接管理器就会使用其中的一个, * 而不是再创建一个新的连接 * @author songyl * @date 2019年8月15日 上午12:51:20 */ private static void configPoolMgr() { try { /**Http连接使用java.net.Socket类来传输数据。这依赖于ConnectionSocketFactory接口来创建、 初始化和连接socket。这样也就允许HttpClient的用户在代码运行时,指定socket初始化的代码**/ ConnectionSocketFactory plainsf = PlainConnectionSocketFactory.getSocketFactory(); //配置通用的SSL TrustStrategy acceptingTrustStrategy = (cert, authType) -> true; SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build(); sslsf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE); //自定义的socket工厂类可以和指定的协议(Http、Https)联系起来,用来创建自定义的连接管理器 Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create() .register("http", plainsf) .register("https", sslsf) .build(); //连接池管理器 connMgr = new PoolingHttpClientConnectionManager(registry); //最大连接数 connMgr.setMaxTotal(MAX_TOTAL); //每个路由基础的连接数 connMgr.setDefaultMaxPerRoute(ROUTE_MAX_TOTAL); } catch (Exception e) { LOGGER.error("configPoolMgr error: ", e); // throw new TbspBaseRuntimeException("连接池配置异常", e); } }
/** * @return * @description 获取HttpClient * 添加 PoolingHttpClientConnectionManager connMgr 连接池 * 添加 HttpRequestRetryHandler retryHandler 重试机制 * @author songyl * @date 2019年8月15日 上午1:08:25 */ private static CloseableHttpClient getHttpClient() { HttpClientBuilder httpClientBuilder = HttpClients.custom(); httpClientBuilder.setConnectionManager(connMgr); httpClientBuilder.setRetryHandler(retryHandler); httpClientBuilder.setSSLSocketFactory(sslsf); //创建httpClient return httpClientBuilder.build(); }
5.最终方案
通过忽略SSL证书在客户现场并没有把问题给解决掉,这个时候我们只能再分析客户现场跟我们本地有什么差异,通过了解客户现场再网络链路上对外请求HTTP增加了一层代理服务器,差异如下,客户现场多了代理服务器这一段网络链路。
因此在通过代理服务器访问第三方目标地址时,需要在setProxy()方法中设置代理IP后可以将url中的域名换成这个代理IP,http很简单,但是https这样会报错。
问题:如何使用代理发送https请求?
客户端发送https请求之前会先向这台服务器请求ssl证书,并在客户端对这个证书做一个校验。
而使用代理IP时,实际上请求打到了这个代理IP上,而客户端并不知道这件事,他仍然在等待url域名中所对应的ssl证书,而这代理IP对应的服务器实际上并没有这个证书,导致了https请求失败。
解决办法:在忽略SSL证书验证的同时应该显式的配置代理地址,通过代理转发到目标地址
// 代理信息 protected static String proxyUrl; /** * @return * @description 获取HttpClient * 添加 PoolingHttpClientConnectionManager connMgr 连接池 * 添加 HttpRequestRetryHandler retryHandler 重试机制 * @author songyl * @date 2019年8月15日 上午1:08:25 */ private static CloseableHttpClient getHttpClient() { HttpClientBuilder httpClientBuilder = HttpClients.custom(); httpClientBuilder.setConnectionManager(connMgr); httpClientBuilder.setRetryHandler(retryHandler); httpClientBuilder.setSSLSocketFactory(sslsf); if (proxyUrl != null) { HttpHost proxy = HttpHost.create(proxyUrl); DefaultProxyRoutePlanner routePlanner = new DefaultProxyRoutePlanner(proxy); httpClientBuilder.setRoutePlanner(routePlanner); } //创建httpClient return httpClientBuilder.build(); }
6.再次回归
再次发布客户现场回归测试后我们发现SSL异常问题已彻底解决,同时通过请求日志我们会发现请求底层会进行一次代理服务器的路由转发
7.问题总结
通过这个问题的分析跟解决我发现在我们平时做一些基础工具的研发过程中,由于我们所服务的客户的特点(客户存在本地化部署&云部署模式)我们需要更多的考虑客户现场跟我们本地的一些网络链路上的差异,尽可能的考虑程序的兼容性及健壮性,在这个问题上我们初期就应该考虑直连与代理访问两种情况。