給HttpClient添加Socks代理


本文描述http client使用socks代理過程中需要注意的幾個方面:1,socks5支持用戶密碼授權;2,支持https;3,支持讓代理服務器解析DNS;

使用代理創建Socket

從原理上來看,不管用什么http客戶端(httpclient,okhttp),最終都要轉換到java.net.Socket的創建上去,看到代碼:

package java.net;
 public Socket(Proxy proxy) {
    ...
 }

這是JDK中對網絡請求使用Socks代理的入口方法。(http代理是在http協議層之上的,不在此文討論范圍之內)。
HttpClient要實現socks代理,就需要塞進去一個Proxy對象,也就是定制兩個類:org.apache.http.conn.ssl.SSLConnectionSocketFactoryorg.apache.http.conn.socket.PlainConnectionSocketFactory,分別對應https和http。
代碼如下:

    private class SocksSSLConnectionSocketFactory extends SSLConnectionSocketFactory {

        public SocksSSLConnectionSocketFactory(SSLContext sslContext, HostnameVerifier hostnameVerifier) {
            super(sslContext, hostnameVerifier);
        }

        @Override
        public Socket createSocket(HttpContext context) throws IOException {
            ProxyConfig proxyConfig = (ProxyConfig) context.getAttribute(ProxyConfigKey);
            if (proxyConfig != null) {//需要代理
                return new Socket(proxyConfig.getProxy());
            } else {
                return super.createSocket(context);
            }
        }

        @Override
        public Socket connectSocket(int connectTimeout, Socket socket, HttpHost host, InetSocketAddress remoteAddress,
                                    InetSocketAddress localAddress, HttpContext context) throws IOException {
            ProxyConfig proxyConfig = (ProxyConfig) context.getAttribute(ProxyConfigKey);
            if (proxyConfig != null) {//make proxy server to resolve host in http url
                remoteAddress = InetSocketAddress
                        .createUnresolved(host.getHostName(), host.getPort());
            }
            return super.connectSocket(connectTimeout, socket, host, remoteAddress, localAddress, context);
        }
    }

    private class SocksSSLConnectionSocketFactory extends SSLConnectionSocketFactory {

        public SocksSSLConnectionSocketFactory(SSLContext sslContext, HostnameVerifier hostnameVerifier) {
            super(sslContext, hostnameVerifier);
        }

        @Override
        public Socket createSocket(HttpContext context) throws IOException {
            ProxyConfig proxyConfig = (ProxyConfig) context.getAttribute(ProxyConfigKey);
            if (proxyConfig != null) {
                return new Socket(proxyConfig.getProxy());
            } else {
                return super.createSocket(context);
            }
        }

        @Override
        public Socket connectSocket(int connectTimeout, Socket socket, HttpHost host, InetSocketAddress remoteAddress,
                                    InetSocketAddress localAddress, HttpContext context) throws IOException {
            ProxyConfig proxyConfig = (ProxyConfig) context.getAttribute(ProxyConfigKey);
            if (proxyConfig != null) {//make proxy server to resolve host in http url
                remoteAddress = InetSocketAddress
                        .createUnresolved(host.getHostName(), host.getPort());
            }
            return super.connectSocket(connectTimeout, socket, host, remoteAddress, localAddress, context);
        }
    }

然后在創建httpclient對象時,給HttpClientConnectionManager設置socketFactoryRegistry

            Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
                .register(Protocol.HTTP.toString(), new SocksConnectionSocketFactory())
                .register(Protocol.HTTPS.toString(), new SocksSSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE))
                .build();

        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
    

讓代理服務器解析域名

場景:運行httpClient的進程所在主機可能並不能上公網,大部分時候,也無法進行DNS解析,這時通常會出現域名無法解析的IO異常,下面介紹怎么避免在客戶端解析域名。

上面有一行代碼非常關鍵:

remoteAddress = InetSocketAddress 
                        .createUnresolved(host.getHostName(), host.getPort());

變量host是你發起http請求的目標主機和端口信息,這里創建了一個未解析(Unresolved)的SocketAddress,在socks協議握手階段,InetSocketAddress信息會原封不動的發送到代理服務器,由代理服務器解析出具體的IP地址。
Socks的協議描述中有個片段:

   The SOCKS request is formed as follows:

        +----+-----+-------+------+----------+----------+
        |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
        +----+-----+-------+------+----------+----------+
        | 1  |  1  | X'00' |  1   | Variable |    2     |
        +----+-----+-------+------+----------+----------+

     Where:

          o  VER    protocol version: X'05'
          o  CMD
             o  CONNECT X'01'
             o  BIND X'02'
             o  UDP ASSOCIATE X'03'
          o  RSV    RESERVED
          o  ATYP   address type of following address
             o  IP V4 address: X'01'
             o  DOMAINNAME: X'03'
             o  IP V6 address: X'04'

代碼按上面方法寫,協議握手發送的是ATYP=X'03',即采用域名的地址類型。否則,HttpClient會嘗試在客戶端解析,然后發送ATYP=X'01'進行協商。當然,大多數時候HttpClient在解析域名的時候就掛了。

https中需要注意的問題

在使用httpclient訪問https網站的時候,經常會遇到javax.net.ssl包中的異常,例如:

Caused by: javax.net.ssl.SSLException: Received fatal alert: internal_error
    at sun.security.ssl.Alerts.getSSLException(Unknown Source) ~[na:1.7.0_80]
    at sun.security.ssl.Alerts.getSSLException(Unknown Source) ~[na:1.7.0_80]

一般需要做幾個設置:

創建不校驗證書鏈的SSLContext

        SSLContext sslContext = null;
        try {
            sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
                @Override
                public boolean isTrusted(X509Certificate[] chain, String authType)
                        throws CertificateException {
                    return true;
                }

            }).build();
        } catch (Exception e) {
            throw new com.aliyun.oss.ClientException(e.getMessage());
        }
        ...
        new SocksSSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE)

創建不校驗域名的HostnameVerifier

public class NoopHostnameVerifier implements javax.net.ssl.HostnameVerifier {

    public static final NoopHostnameVerifier INSTANCE = new NoopHostnameVerifier();

    @Override
    public boolean verify(final String s, final SSLSession sslSession) {
        return true;
    }
}

如何使用用戶密碼授權?

java SDK中給Socks代理授權有點特殊,不是按socket來的,而是在系統層面做的全局配置。比如,可以通過下面代碼設置一個全局的Authenticator:

Authenticator.setDefault(new MyAuthenticator("userName", "Password"));
...
class MyAuthenticator extends java.net.Authenticator {
    private String user ;
    private String password ;
  
    public MyAuthenticator(String user, String password) {
      this.user = user;
      this.password = password;
    }
  
    protected PasswordAuthentication getPasswordAuthentication() {
      return new PasswordAuthentication(user, password.toCharArray());
    }
  }

這種方法很簡單,不過有些不方便的地方,如果你的產品中需要連接不同的Proxy服務器,而他們的用戶名密碼是不一樣的,那么這個方法就不適用了。

基於ThreadLocal的Authenticator

public class ThreadLocalProxyAuthenticator extends Authenticator{
    private ThreadLocal<PasswordAuthentication> credentials = null;
     private static class SingletonHolder {
        private static final ThreadLocalProxyAuthenticator instance = new ThreadLocalProxyAuthenticator();
    }
    public static final ThreadLocalProxyAuthenticator getInstance() {
        return SingletonHolder.instance;
    }
      public void setCredentials(String user, String password) {
        credentials.set(new PasswordAuthentication(user, password.toCharArray()));
    }
    public static void clearCredentials() {
        ThreadLocalProxyAuthenticator authenticator = ThreadLocalProxyAuthenticator.getInstance();
        Authenticator.setDefault(authenticator);
        authenticator.credentials.set(null);
    }
    public PasswordAuthentication getPasswordAuthentication() {
        return credentials.get();
    }
}

這個類意味着,授權信息只會保存到當前調用者的線程中,其他線程的調用者無法訪問,在創建Socket的線程中設置密鑰和清理密鑰,就可以做到授權按照Socket連接進行隔離。Java TheadLocal相關知識本文不贅述。

按連接隔離的授權

 class ProxyHttpClient extends CloseableHttpClient{
    private CloseableHttpClient httpClient;
    public ProxyHttpClient(CloseableHttpClient httpClient){
        this.httpClient=httpClient;
    }
    protected CloseableHttpResponse doExecute(HttpHost target, HttpRequest request, HttpContext context) throws IOException, ClientProtocolException {
            ProxyConfig proxyConfig = //這里獲取當前連接的代理配置信息
            boolean clearCredentials = false;
            if (proxyConfig != null) {
                if (context == null) {
                    context = HttpClientContext.create();
                }
                context.setAttribute(ProxyConfigKey, proxyConfig);
                if (proxyConfig.getAuthentication() != null) {
                    ThreadLocalProxyAuthenticator.setCredentials(proxyConfig.getAuthentication());//設置授權信息
                    clearCredentials = true;
                }
            }
            try {
                return httpClient.execute(target, request, context);
            } finally {
                if (clearCredentials) {//清理授權信息
                    ThreadLocalProxyAuthenticator.clearCredentials();
                }
            }
        }
 }

另外,線程是可以復用的,因為每次調用完畢后,都清理了授權信息。
這里有個一POJO類ProxyConfig,保存的是socks代理的IP端口和用戶密碼信息。

public class ProxyConfig {
    private Proxy proxy;
    private PasswordAuthentication authentication;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM