Apache HttpClient 与 SSL 代理

有这样一个场景,出于安全的考虑,某些操作敏感数据的客户端必须通过 VPN 访问服务器端。这种客户端我们姑且称之为代理访问。访问路由示意图:

HTTPS Client <------- Encrypted CONNECT Requests -------> HTTPS Proxy <------- Encrypted CONNECT Requests -------> HTTPS End-Site

代理地址及端口号作为 Property 参数注入 jvm 进程:https.proxyHosthttps.proxyPort

而大部分客户端不需要 VPN 访问服务器端,只需要通过 HTTPS 直接访问即可。这种客户端我们姑且称之为直连访问。访问路由示意图:

HTTPS Client <------- Encrypted CONNECT Requests -------> HTTPS End-Site
注意:
  • 代理配置是以 global 方式提供,也就是说服务器是同一个,所有被分发的客户端启动的时候都有上述 Property 参数
  • 这种 VPN 代理跟 LB 代理不同之处在于 proxy 不需要配置 SSL 证书,也就是说 VPN 只负责服务的监听和转发

Solution 1:使用 JVM 原生态 java.net 和 javax.net.ssl 工具包

示意代码:

    URL localURL = new URL(urlPath);
    URLConnection connection = localURL.openConnection();
    HttpURLConnection httpURLConnection = (HttpURLConnection) connection;
    if (connection instanceof HttpsURLConnection) {
        TrustManager[] tm = {ignoreCertificationTrustManger};
        try {
            SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
            sslContext.init(null, tm, new java.security.SecureRandom());
            SSLSocketFactory ssf = sslContext.getSocketFactory();
            ((HttpsURLConnection) httpURLConnection).setSSLSocketFactory(ssf);
            ((HttpsURLConnection) httpURLConnection).setHostnameVerifier(ignoreHostnameVerifier);
        } catch (NoSuchAlgorithmException e1) {
            logger.logError(e1.getMessage(), e1);
        } catch (NoSuchProviderException e1) {
            logger.logError(e1.getMessage(), e1);
        } catch (KeyManagementException e1) {
            logger.logError(e1.getMessage(), e1);
        }
    }
    httpURLConnection.setDoOutput(true);
    httpURLConnection.setRequestMethod("POST");
    httpURLConnection.setRequestProperty("Content-Type", "application/octet-stream");
    httpURLConnection.setRequestProperty("Accept-Encoding", "chunck");
    httpURLConnection.setConnectTimeout(3000);
    outputStream = httpURLConnection.getOutputStream();

优点:

  • 能自动识别 https.proxyHosthttps.proxyPort 等 Property
  • 能自动识别直连、代理访问环境,然后决定是否用进程内 Property 挂代理访问

缺点:

  • 单次访问性能差于 Apache HttpClient 工具包,性能减一
  • 连接池化管理差,性能再减一

总之,玩玩或交流学习可以,用于生产环境太儿戏。

Solution 2:使用 Apache HttpClient 工具包的 RequestConfig

示意代码:

	HttpHost target = new HttpHost("defonds.net", 443, "https");
	HttpHost proxy = new HttpHost("191.168.1.303", 7443, "https");
	RequestConfig config = RequestConfig.custom()
        .setProxy(proxy)
        .build();
	HttpGet request = new HttpGet("/");
	request.setConfig(config);
	CloseableHttpResponse response = httpclient.execute(target, request);

参考自 Apache 官方示例代码:https://hc.apache.org/httpcomponents-client-ga/httpclient/examples/org/apache/http/examples/client/ClientExecuteProxy.java
直连环境可以,代理环境歇菜。有类似于以下的 SSL 握手问题:

{tls}->https://191.168.1.303:7443->https://defonds.net:443 Connection reset java.net.SocketException: Connection reset at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1367)

Solution 3:使用 Apache HttpClient 工具包的 HttpRoutePlanner

示意代码:

HttpRoutePlanner routePlanner = new HttpRoutePlanner() {
    public HttpRoute determineRoute(
            HttpHost target,
            HttpRequest request,
            HttpContext context) throws HttpException {
        return new HttpRoute(target, null,  new HttpHost("someproxy", 8080),
                "https".equalsIgnoreCase(target.getSchemeName()));
    }
};
CloseableHttpClient httpclient = HttpClients.custom()
        .setRoutePlanner(routePlanner)
        .build();
    }
}

参考自 Apache 官方示例代码:http://hc.apache.org/httpcomponents-client-4.5.x/tutorial/html/connmgmt.html#d5e485
上述代码在代理环境可以,直连环境还需自行适配。
优点:

  • 底层做过传输优化,单次性能高于原生态工具包
  • 池化管理高效高性能

缺点:

  • 不能自动识别 https.proxyHosthttps.proxyPort 等 Property
  • 代理 / 直连需显式适配

参考资料

posted @ 2019-10-09 22:00  Defonds  阅读(113)  评论(0编辑  收藏  举报