给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等类见上一篇博客。

OkHttp3 是一个强大的 HTTP 客户端库,它提供了灵活的 API 和丰富的功能,包括支持网络连接池、断点续传、缓存控制等。要在 Spring Boot 中使用 OkHttp3 实现动态代理,通常是为了方便进行性能测试、日志跟踪或安全控制,例如使用像 Charles 或 WireShark 这样的代理服务器。 以下是一个简单的步骤来配置 OkHttp3 使用动态代理: 1. 添加依赖:首先,在你的 `build.gradle` 文件中添加 OkHttp3 的依赖(如果你还没有添加): ```groovy implementation 'com.squareup.okhttp3:okhttp:4.9.3' ``` 2. 配置代理:在 Spring Boot 的配置类(如 `ApplicationConfig` 或 `OkHttpClientConfig`)中设置 OkHttpClient代理: ```java import okhttp3.OkHttpClient; import okhttp3.Proxy; import okhttp3.logging.HttpLoggingInterceptor; @Configuration public class OkHttpClientConfig { @Value("${proxy.host:}") private String proxyHost; @Value("${proxy.port:}") private int proxyPort; @Bean public OkHttpClient httpClient() { OkHttpClient.Builder builder = new OkHttpClient.Builder(); if (StringUtils.isNotBlank(proxyHost) && proxyPort > 0) { Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)); builder.proxy(proxy); HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(); loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); builder.addInterceptor(loggingInterceptor); } // 其他配置,如连接池、超时设置等 return builder.build(); } } ``` 这里假设你已经有了一个环境变量 `proxy.host` 和 `proxy.port` 来定义代理服务器的地址和端口。如果没有,你需要根据实际情况提供这些值。 3. 注册为 RestTemplate 或其他依赖的默认客户端:如果你打算使用 OkHttp3 作为 Spring Web 的 `RestTemplate` 或其他客户端的默认实现,你可以将 OkHttpClient 注册到 `WebClient` 或 `RestTemplate` 中: ```java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.client.OkHttp3ClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; @Autowired public void configureRestTemplate(OkHttpClient httpClient) { RestTemplate restTemplate = new RestTemplate(new OkHttp3ClientHttpRequestFactory(httpClient)); // 使用 restTemplate 发送 HTTP 请求 } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值