Okhttp的使用沒有httpClient廣泛,網上關於Okhttp設置代理的方法很少,這篇文章完整介紹了需要註意的方方面面。 上一篇博客中介紹了socks代理的入口是創建 時傳入一個 對象。 OkHttp client通過 創建,可以通過定製 和`java.net.SocketFactory`來實 ...
Okhttp的使用沒有httpClient廣泛,網上關於Okhttp設置代理的方法很少,這篇文章完整介紹了需要註意的方方面面。
上一篇博客中介紹了socks代理的入口是創建java.net.Socket
時傳入一個java.net.Porxy
對象。 OkHttp client通過OkHttpClient.Builder
創建,可以通過定製javax.net.ssl.SSLSocketFactory
和java.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
等類見上一篇博客。