天天看點

Apache HttpClient 與 SSL 代理

有這樣一個場景,出于安全的考慮,某些操作敏感資料的用戶端必須通過 VPN 通路伺服器端。這種用戶端我們姑且稱之為代理通路。通路路由示意圖:

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

代理位址及端口号作為 Property 參數注入 jvm 程序:https.proxyHost、https.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 工具包

示意代碼:

= 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.proxyHost、https.proxyPort等 Property
  • 能自動識别直連、代理通路環境,然後決定是否用程序内 Property 挂代理通路

缺點:

  • 單次通路性能差于 Apache HttpClient 工具包,性能減一
  • 連接配接池化管理差,性能再減一

總之,玩玩或交流學習可以,用于生産環境太兒戲。

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

示意代碼:

= 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.proxyHost、https.proxyPort等 Property
  • 代理 / 直連需顯式适配

參考資料

  • ​​HttpRoutePlanner - How does it work with an HTTPS Proxy​​
  • ​​HttpClient proxy configuration​​