本文描述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.SSLConnectionSocketFactory 和org.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;
}

JAVA知识积累 给HttpClient添加Socks代理的更多相关文章

  1. 给HttpClient添加Socks代理

    本文描述http client使用socks代理过程中需要注意的几个方面:1,socks5支持用户密码授权:2,支持https:3,支持让代理服务器解析DNS: 使用代理创建Socket 从原理上来看 ...

  2. 给OkHttp Client添加socks代理

    Okhttp的使用没有httpClient广泛,网上关于Okhttp设置代理的方法很少,这篇文章完整介绍了需要注意的方方面面. 上一篇博客中介绍了socks代理的入口是创建java.net.Socke ...

  3. Java知识积累3-XML的DOM解析修改和删除方法

    import java.io.File; import java.io.IOException; import javax.xml.parsers.DocumentBuilder;import jav ...

  4. Java知识积累-XML的DOM解析修改和删除方法

    import java.io.File; import java.io.IOException; import javax.xml.parsers.DocumentBuilder;import jav ...

  5. Java知识积累2-StringReverse实现文字(单词)倒叙输出

    package String; import java.util.Stack;import java.util.StringTokenizer; public class StringReverse ...

  6. Java知识积累1-StringAlign实现文字居中左右对齐

    import java.text.*;public class StringAlign extends Format{ public static final int JUST_LEFT='l'; / ...

  7. 项目积累——JAVA知识积累

    调用天气: <iframe src="http://www.thinkpage.cn/weather/weather.aspx?uid=&c=CHXX0008&l=zh ...

  8. java知识积累——单元测试和JUnit(二)

    首先来复习一下几个重要知识点,然后接着进行一些介绍.在上一篇文章中,我曾经贴过下面这张图片: 在Which method stubs would you like to create?这里,现在结合4 ...

  9. Java知识积累——单元测试和JUnit(一)

    说起单元测试,刚毕业或者没毕业的人可能大多停留在课本讲述的定义阶段,至于具体是怎么定义的,估计也不会有太多人记得.我们的教育总是这样让人“欣 慰”.那么什么是单元测试呢?具体科学的定义咱就不去关心了, ...

随机推荐

  1. 关于ie6下png背景透明

    今天我突破了一个技术难关,真的是头都大了.. 关于ie6下png背景透明的解决方法,我就不多说了,网上有很多解决方法,我用的是其中的一种: <script type="text/jav ...

  2. 安卓手机安装虚拟定位的方法Xposed安装器+模拟位置(Xposed模块)

    原文:https://www.52pojie.cn/thread-571328-1-1.html 未测试,据说只支持某些手机,小米和华为很难安装,建议买其他品牌. Xposed安装器步骤:·ROOT你 ...

  3. MySQL ALTER讲解

    当我们需要修改数据表名或者修改数据表字段时,就需要使用到MySQL ALTER命令. 开始本章教程前让我们先创建一张表,表名为:testalter_tbl. root@host# mysql -u r ...

  4. 中国标准时间改为formatTime格式

    1.toLocaleDateString (根据本地时间把Date 对象的日期部分转换为字符串): var time = new Date(); var formatTime = time.toLoc ...

  5. cs 更新

    CSS介绍 CSS(Cascading Style Sheet,层叠样式表)定义如何显示HTML元素. 当浏览器读到一个样式表,它就会按照这个样式表来对文档进行格式化(渲染). CSS语法 CSS实例 ...

  6. django中使用Form组件

    内容: 1.Form组件介绍 2.Form组件常用字段 3.Form组件校验功能 4.Form组件内置正则校验 参考:https://www.cnblogs.com/liwenzhou/p/87478 ...

  7. selenium+python自动化93-Chrome报错:Python is likely shutting down

    遇到问题 报错信息:sys.meta_path is None, Python is likely shutting down 1.我的环境: python 3.6 selenium 2.53.6 c ...

  8. Static / Const 的概念

    C/C++/Java Static / Const 的概念 这里以C为准,其他语言类似. Static变量是指分配不变(只可分配一次,以后再分配就无效了.)的变量 -- 它的存活寿命或伸展域可以贯穿程 ...

  9. UVA-755-排序

    奇怪,我怎么还有一个排序题目没过 题意如下: 公司喜欢有难忘的电话号码,一个让电话号码变得难忘的方式是有一个拼读起来难忘的单词,比如,你可以呼叫University of Waterloo通过拨打难忘 ...

  10. CUDA C Programming Guide 在线教程学习笔记 Part 3

    ▶ 表面内存使用 ● 创建 cuda 数组时使用标志 cudaArraySurfaceLoadStore 来创建表面内存,可以用表面对象(surface object)或表面引用(surface re ...