• 给OkHttp Client添加socks代理


    Okhttp的使用没有httpClient广泛,网上关于Okhttp设置代理的方法很少,这篇文章完整介绍了需要注意的方方面面。

    上一篇博客中介绍了socks代理的入口是创建java.net.Socket时传入一个java.net.Porxy对象。 OkHttp client通过OkHttpClient.Builder创建,可以通过定制javax.net.ssl.SSLSocketFactoryjava.net.SocketFactory来实现socks代理。

    定制SocketFactory

    JDK中默认的是java.net.DefaultSocketFactory,它是包可见的,没法扩展,所以只能用java.net.SocketFactory扩展,没有什么其他好的招数,这个过程其实有点繁琐。

    public class ProxySocketFactory extends SocketFactory {
    
        private ProxyConfigProvider proxyConfigProvider;
    
        public ProxySocketFactory(ProxyConfigProvider proxyConfigProvider) {
            this.proxyConfigProvider = proxyConfigProvider;
        }
    
        @Override
        public Socket createSocket() throws IOException {
            ProxyConfig proxyConfig = proxyConfigProvider.getProxyConfig();
            if (proxyConfig != null) {
                return new Socket(proxyConfig.getProxy());
            } else {
                return new Socket();
            }
        }
    
        public Socket createSocket(String host, int port)
                throws IOException, UnknownHostException {
            Socket socket = createSocket();
            try {
                socket.connect(new InetSocketAddress(host, port));
            } catch (IOException e) {
                socket.close();
                throw e;
            }
            return socket;
        }
    
        public Socket createSocket(InetAddress address, int port)
                throws IOException {
            Socket socket = createSocket();
            try {
                socket.connect(new InetSocketAddress(address, port));
            } catch (IOException e) {
                socket.close();
                throw e;
            }
            return socket;
        }
    
        public Socket createSocket(String host, int port,
                                   InetAddress clientAddress, int clientPort)
                throws IOException, UnknownHostException {
            Socket socket = createSocket();
            try {
                socket.bind(new InetSocketAddress(clientAddress, clientPort));
                socket.connect(new InetSocketAddress(host, port));
            } catch (IOException e) {
                socket.close();
                throw e;
            }
            return socket;
        }
    
        public Socket createSocket(InetAddress address, int port,
                                   InetAddress clientAddress, int clientPort)
                throws IOException {
            Socket socket = createSocket();
            try {
                socket.bind(new InetSocketAddress(clientAddress, clientPort));
                socket.connect(new InetSocketAddress(address, port));
            } catch (IOException e) {
                socket.close();
                throw e;
            }
            return socket;
        }
    }
    
    

    上面代码这么长,并不是随便写的,也省不了。我参考了DefaultSocketFactory里面创建Socket对象的各个构造函数,保证了ProxySocketFactory的跟DefaultSocketFactory对应方法的行为完全一致,只是在创建socket时用了代理而已。简单一句话:只要传入了IP地址和端口,就会直接发起连接。

    对SSL socket工厂的定制同样繁琐。不是简单的继承(继承搞不定),而是采用包装模式,用原来的SSLSocketFactory来实现与代理无关的方法。

    public class ProxySSLSocketFactory extends SSLSocketFactory {
        private ProxyConfigProvider configProvider;
        private SSLSocketFactory socketFactory;
    
        public ProxySSLSocketFactory(ProxyConfigProvider configProvider, SSLSocketFactory socketFactory) {
            this.configProvider = configProvider;
            this.socketFactory = socketFactory;
        }
    
        @Override
        public String[] getDefaultCipherSuites() {
            return socketFactory.getDefaultCipherSuites();
        }
    
        @Override
        public String[] getSupportedCipherSuites() {
            return socketFactory.getSupportedCipherSuites();
        }
    
        public Socket createSocket()
                throws IOException {
            ProxyConfig proxyConfig = configProvider.getProxyConfig();
            if (proxyConfig != null) {
                return new Socket(proxyConfig.getProxy());
            } else {
                return new Socket();
            }
        }
    
        public Socket createSocket(String host, int port)
                throws IOException {
            Socket socket = createSocket();
            try {
                return socketFactory.createSocket(socket, host, port, true);
            } catch (IOException e) {
                socket.close();
                throw e;
            }
        }
    
        public Socket createSocket(Socket s, String host,
                                   int port, boolean autoClose)
                throws IOException {
            //TODO 无法代理
            return socketFactory.createSocket(s, host, port, autoClose);
        }
    
        public Socket createSocket(InetAddress address, int port)
                throws IOException {
            Socket socket = createSocket();
            try {
                return socketFactory.createSocket(socket, address.getHostAddress(), port, true);
            } catch (IOException e) {
                socket.close();
                throw e;
            }
        }
    
        public Socket createSocket(String host, int port,
                                   InetAddress clientAddress, int clientPort)
                throws IOException {
            Socket socket = createSocket();
            try {
                socket.bind(new InetSocketAddress(clientAddress, clientPort));
                return socketFactory.createSocket(socket, host, port, true);
            } catch (IOException e) {
                socket.close();
                throw e;
            }
        }
    
        public Socket createSocket(InetAddress address, int port,
                                   InetAddress clientAddress, int clientPort)
                throws IOException {
            Socket socket = createSocket();
            try {
                socket.bind(new InetSocketAddress(clientAddress, clientPort));
                return socketFactory.createSocket(socket, address.getHostAddress(), port, true);
            } catch (IOException e) {
                socket.close();
                throw e;
            }
        }
    }
    

    注意,其中有一个方法是无法使用代理的:Socket createSocket(Socket s, String host,int port, boolean autoClose),因为传入的已经是一个创建好的Socket,所以使用这个方法,要注意你的组件有没有用到这个方法,如果用到了,代理的功能需要在这个方法的调用者那一层去实现。我测试OkHttp client是没有用到这个方法的。

    创建OkHttpClient对象

    两个工厂类设计好了之后,下面就是运用他们创建OkHttpClient对象。

        public OkHttpClient buildOkHttpClient() {
            OkHttpClient httpClient = new OkHttpClient.Builder()
                    .sslSocketFactory(new ProxySSLSocketFactory(proxyProvider, NOP_TLSV12_SSL_CONTEXT.getSocketFactory()), NOP_TRUST_MANAGER)
                    .socketFactory(new ProxySocketFactory(proxyProvider))
                    .hostnameVerifier(NOP_HOSTNAME_VERIFIER)
                    .connectTimeout(DEFAULT_CONNECTION_TIMEOUT, TimeUnit.SECONDS)
                    .writeTimeout(DEFAULT_CONNECTION_TIMEOUT, TimeUnit.SECONDS)
                    .readTimeout(DEFAULT_CONNECTION_TIMEOUT, TimeUnit.SECONDS)                
                    .addInterceptor(new OkHttpProxyInterceptor())
                    .build();
            return httpClient;
        }
    

    上面的方法注意两行:

    .sslSocketFactory(xxx)
    .socketFactory(xxx)
    

    其他代码,有对超时的设置.xxxTimeout(),这里不赘述,还有对https请求证书的设置以及对socks密码的设置,稍后详解

    设置https需要注意的地方

    NOP_TRUST_MANAGER对象设置为信任任何证书:

        public static final X509TrustManager NOP_TRUST_MANAGER = new X509TrustManager() {
            @Override
            public void checkClientTrusted(X509Certificate[] x509Certificates, String s)
                    throws CertificateException {
            }
    
            @Override
            public void checkServerTrusted(X509Certificate[] x509Certificates, String s)
                    throws CertificateException {
            }
    
            @Override
            public X509Certificate[] getAcceptedIssuers() {
                return new X509Certificate[0];
            }
        };
    

    NOP_HOSTNAME_VERIFIER对象设置为信任任何域名:

        public static final HostnameVerifier NOP_HOSTNAME_VERIFIER = new HostnameVerifier() {
            @Override
            public boolean verify(String s, SSLSession sslSession) {
                return true;
            }
        };
    

    https的协议版本可能不支持TLSv1

    上面两条设置网上很多文档都会介绍,重点需要关注的是NOP_TLSV12_SSL_CONTEXT对象。

    干货来了:JDK使用的HTTPS协议版本参考这里-diagnosing-tls,-ssl,-and-https,可以看到在JDK7以前,默认都是TLSv1,JDK8才默认采用TLSv1.2,而很多较新的网站都已经禁用HTTPS使用TLSv1握手了,认为这个协议已经不够安全(例如,我在给minio-java client添加proxy支持时,发现minio-server就这么干的)。所以你用java去发起HTTPS连接经常会出现SSL相关的异常,大部分异常信息根本看不懂。

    所以,需要在代码里面指定https使用TLSv1.2:

        static {
            try {
                NOP_TLSV12_SSL_CONTEXT = SSLContext.getInstance("TLSv1.2");
                NOP_TLSV12_SSL_CONTEXT.init(null, new TrustManager[]{NOP_TRUST_MANAGER}, new java.security.SecureRandom());
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            } catch (KeyManagementException e) {
                e.printStackTrace();
            }
        }
    

    实际上也可以通过:

    System.setProperty("https.protocols","TLSv1.2");//在创建任何SSLSocketFactory之前调用
    

    或者设置虚拟机参数:

    -Dhttps.protocols=TLSv1.2,TLSv1.1,TLSv1
    

    放在前面的协议会被优先选用

    检测https支持的协议版本

    如果出现SSL相关异常,但是又不确定是不是协议版本导致的,可以有几个工具进行检验:
    nmap : nmap --script ssl-enum-ciphers -p 443 baidu.com,在我机器上看到:

    PORT    STATE SERVICE
    443/tcp open  https
    | ssl-enum-ciphers: 
    |   SSLv3: 
    |     ciphers: 
    |       TLS_ECDHE_RSA_WITH_RC4_128_SHA (secp256r1) - A
    |       TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (secp256r1) - A
    |       TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (secp256r1) - A
    |       TLS_RSA_WITH_AES_128_CBC_SHA (rsa 2048) - A
    |       TLS_RSA_WITH_AES_256_CBC_SHA (rsa 2048) - A
    |       TLS_RSA_WITH_RC4_128_SHA (rsa 2048) - A
    |     compressors: 
    |       NULL
    |     cipher preference: server
    |     warnings: 
    |       CBC-mode cipher in SSLv3 (CVE-2014-3566)
    |   TLSv1.0: 
    |     ciphers: 
    |       TLS_ECDHE_RSA_WITH_RC4_128_SHA (secp256r1) - A
    |       TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (secp256r1) - A
    |       TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (secp256r1) - A
    |       TLS_RSA_WITH_AES_128_CBC_SHA (rsa 2048) - A
    |       TLS_RSA_WITH_AES_256_CBC_SHA (rsa 2048) - A
    |       TLS_RSA_WITH_RC4_128_SHA (rsa 2048) - A
    |     compressors: 
    |       NULL
    |     cipher preference: server
    |   TLSv1.1: 
    |     ciphers: 
    |       TLS_ECDHE_RSA_WITH_RC4_128_SHA (secp256r1) - A
    |       TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (secp256r1) - A
    |       TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (secp256r1) - A
    |       TLS_RSA_WITH_AES_128_CBC_SHA (rsa 2048) - A
    |       TLS_RSA_WITH_AES_256_CBC_SHA (rsa 2048) - A
    |       TLS_RSA_WITH_RC4_128_SHA (rsa 2048) - A
    |     compressors: 
    |       NULL
    |     cipher preference: server
    |     warnings: 
    |       Weak cipher RC4 in TLSv1.1 or newer not needed for BEAST mitigation
    |   TLSv1.2: 
    |     ciphers: 
    |       TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (secp256r1) - A
    |       TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 (secp256r1) - A
    |       TLS_ECDHE_RSA_WITH_RC4_128_SHA (secp256r1) - A
    |       TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (secp256r1) - A
    |       TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (secp256r1) - A
    |       TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (secp256r1) - A
    |       TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 (secp256r1) - A
    |       TLS_RSA_WITH_AES_128_GCM_SHA256 (rsa 2048) - A
    |       TLS_RSA_WITH_AES_256_GCM_SHA384 (rsa 2048) - A
    |       TLS_RSA_WITH_AES_128_CBC_SHA256 (rsa 2048) - A
    |       TLS_RSA_WITH_AES_128_CBC_SHA (rsa 2048) - A
    |       TLS_RSA_WITH_AES_256_CBC_SHA256 (rsa 2048) - A
    |       TLS_RSA_WITH_AES_256_CBC_SHA (rsa 2048) - A
    |       TLS_RSA_WITH_RC4_128_SHA (rsa 2048) - A
    |     compressors: 
    |       NULL
    |     cipher preference: server
    |     warnings: 
    |       Weak cipher RC4 in TLSv1.1 or newer not needed for BEAST mitigation
    |_  least strength: A
    

    设置Socks的用户名和密码

    OkHttp提供了拦截器的机制okhttp3.Interceptor,可以定制http发起的流程。
    设置密码有两种方式:一种是设置全局的,另一种是按线程隔离(也是就是按请求隔离),不同请求可以设置不同的用户名和密码,详细介绍以及部分代码见我的上一篇博客-给HttpClient添加Socks代理
    注意到上面代码有这么一行:

    .addInterceptor(new OkHttpProxyInterceptor())
    

    就是设置一个拦截器。

        private class OkHttpProxyInterceptor implements Interceptor {
            @Override
            public Response intercept(Chain chain) throws IOException {
                ProxyConfig proxyConfig = ProxyHttpClientBuilder.this.getProxyConfig();
                boolean clearCredentials = false;
                if (proxyConfig != null) {
                    if (proxyConfig.getAuthentication() != null) {
                        ThreadLocalProxyAuthenticator.setCredentials(proxyConfig.getAuthentication());
                        clearCredentials = true;
                    }
                }
                try {
                    return chain.proceed(chain.request());
                } finally {
                    if (clearCredentials) {
                        ThreadLocalProxyAuthenticator.clearCredentials();
                    }
                }
            }
        }
    

    ThreadLocalProxyAuthenticator,ProxyConfig等类见上一篇博客。

  • 相关阅读:
    RequireJS 和 Sea.js
    zoom:1
    font-sqirrel
    WEB前端面试题 分别使用2个、3个、5个DIV画出一个大的红十字
    获取表单select域的选择部分的文本
    写一个简单的form表单,当光标离开表单的时候表单的值发送给后台
    Python3基础 e记法示例
    Python3基础 response.read 输出网页的源代码
    Python3基础 访问在线的有道词典
    Python3基础 response.info 服务器返回的header信息
  • 原文地址:https://www.cnblogs.com/cwjcsu/p/add-socks-proxy-support-for-okhttp.html
Copyright © 2020-2023  润新知