给OkHttp Client添加socks代理

给OkHttp Client添加socks代理Okhttp 的使用没有 httpClient 广泛 网上关于 Okhttp 设置代理的方法很少 这篇文章完整介绍了需要注意的方方面面 上一篇博客中介绍了 socks 代理的入口是创建 java net Socket 时传入一个 java net Porxy 对象 OkHttpclient 通过 OkHttpClient Builder 创建 可以通过定制 javax net ssl SSLSocketFac 和

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支持的协议版本

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

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/229722.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月16日 下午4:02
下一篇 2026年3月16日 下午4:02


相关推荐

  • 地理空间数据云DEM数据解压失败_解决了

    地理空间数据云DEM数据解压失败_解决了准备计算土壤保持生态系统服务 需要一个分水岭矢量图 所以先从空间数据云下 Dem 发现 SRTNSLOPE90M 下载好了解压的时候会报错 搜了一下看到有人说直接解压到别的盘发现没用 后来去搜了一下 img 格式在 arcgis 中怎么打开 才知道这种格式不需要解压直接可以打开

    2026年1月15日
    2
  • 浮动IP(FLOAT IP)

    浮动IP(FLOAT IP)这篇博文主要讲解关于浮动 ip 的相关内容 主要介绍为什么要有浮动 ip 什么情况下需要使用浮动 ip 以及浮动 ip 是什么意思 1 为什么要有浮动 IP 这个东西 nbsp nbsp nbsp nbsp 现在有一个场景 在一台 Linux 上部署一个 web 应用 应用跑在 tomcat 里面 linux 网卡上的 ip 是 192 168 133 100 大致就是如下的部署关系

    2026年3月20日
    2
  • 在Docker中运行OpenClaw

    在Docker中运行OpenClaw

    2026年3月15日
    3
  • java的定时器用法

    java的定时器用法

    2021年12月4日
    45
  • vue中清除浏览器缓存得方法

    vue中清除浏览器缓存得方法1.在HTTP协议中,只有后端返回expires或Cache-Control:max-age=XXX,前端才缓存。但在浏览器中,默认会对htmlcssjs等静态文件、以及重定向进行缓存,如果在HEAD头中指定:<HEAD> <METAHTTP-EQUIV=”Pragma”CONTENT=”no-cache”> <METAHTTP-EQUIV=”Cache-Control”CONTENT=”no-cache”> <METAHTTP-EQUIV

    2022年7月18日
    13
  • centos7安装方法_ad9安装及激活成功教程教程

    centos7安装方法_ad9安装及激活成功教程教程本文超详细的将CentOS7的安装过程做了详细的记录,从下载镜像文件到安装CentOS再到最后的配置,手把手教学,保证能够顺利的将CentOS安装好、配置好。

    2022年10月4日
    5

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号