Java 使用 OkHttp 对接 SOCKS5 代理的一个坑:为什么代理 IP 没生效?

背景

项目后端需要对接第三方接口,但第三方配置了 IP 白名单。本地开发环境的公网 IP 不在白名单内,无法直接请求第三方接口。

为了解决本地调试问题,我们在云服务器上搭建了一个 SOCKS5 代理服务。本地 Java 程序请求第三方接口时,通过 SOCKS5 代理转发,这样第三方看到的请求来源就是云服务器 IP。

整体链路如下:

本地 Java 程序
  -> SOCKS5 代理服务器
  -> 第三方接口

但在测试时发现,使用 OkHttp 配置 SOCKS5 代理后,请求 https://httpbin.org/ip 并没有正确返回代理服务器的出口 IP。

问题代码

一开始的代理工具类大概是这样:

public class ProxyManager {

	private static volatile OkHttpClient proxyClient;
	private static final Object lock = new Object();

	public static OkHttpClient getProxyClient() {
		if (proxyClient == null) {
			synchronized (lock) {
				if (proxyClient == null) {
					String proxyHost = "代理服务器IP";
					int proxyPort = 10000;

					String proxyUser = "代理账号";
					String proxyPass = "代理密码";

					Proxy proxy = new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(proxyHost, proxyPort));
					proxyClient = new OkHttpClient.Builder()
						.proxy(proxy)
						.connectTimeout(30, TimeUnit.SECONDS)
						.readTimeout(30, TimeUnit.SECONDS)
						.build();
				}
			}
		}
		return proxyClient;
	}
}

表面上看,代理 IP、端口、账号、密码都写了。但实际问题在于:proxyUserproxyPass 只是普通局部变量,并没有被任何地方使用。

也就是说,这段代码只配置了 SOCKS5 代理地址,没有完成 SOCKS5 认证。

核心原因

OkHttp 对代理认证有一个容易踩坑的点:

OkHttpClient.Builder.proxyAuthenticator(...) 主要用于 HTTP 代理认证,不负责 SOCKS5 代理的用户名密码认证。

SOCKS5 的认证通常由 JDK 底层 Socket 处理,需要通过 java.net.Authenticator 注册认证信息。

所以,如果只是这样:

String proxyUser = "代理账号";
String proxyPass = "代理密码";

这两个变量不会自动生效。

正确写法

可以通过 Authenticator.setDefault(...) 注册 SOCKS5 认证信息,并且一定要限制代理地址和端口,避免影响 JVM 内其它网络请求。

package org.springblade.common.tool;

import okhttp3.OkHttpClient;

import java.net.Authenticator;
import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.Proxy;
import java.util.concurrent.TimeUnit;

/**
 * SOCKS5代理客户端管理器。
 */
public class ProxyManager {

	private static volatile OkHttpClient proxyClient;
	private static final Object LOCK = new Object();

	private static final String PROXY_HOST = "代理服务器IP";
	private static final int PROXY_PORT = 10000;
	private static final String PROXY_USER = "代理账号";
	private static final String PROXY_PASS = "代理密码";

	/**
	 * 获取代理专用的OkHttpClient。
	 */
	public static OkHttpClient getProxyClient() {
		if (proxyClient == null) {
			synchronized (LOCK) {
				if (proxyClient == null) {
					registerSocksAuthenticator();
					Proxy proxy = new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(PROXY_HOST, PROXY_PORT));
					proxyClient = new OkHttpClient.Builder()
						.proxy(proxy)
						.connectTimeout(30, TimeUnit.SECONDS)
						.readTimeout(30, TimeUnit.SECONDS)
						.build();
				}
			}
		}
		return proxyClient;
	}

	/**
	 * 注册SOCKS5代理认证。
	 */
	private static void registerSocksAuthenticator() {
		Authenticator.setDefault(new Authenticator() {
			@Override
			protected PasswordAuthentication getPasswordAuthentication() {
				if (RequestorType.PROXY == getRequestorType()
					&& PROXY_HOST.equals(getRequestingHost())
					&& PROXY_PORT == getRequestingPort()) {
					return new PasswordAuthentication(PROXY_USER, PROXY_PASS.toCharArray());
				}
				return null;
			}
		});
	}
}

测试方式

可以用下面的方式验证代理出口 IP:

@Test
public void testProxyIp() throws IOException {
	OkHttpClient client = ProxyManager.getProxyClient();

	Request request = new Request.Builder()
		.url("https://httpbin.org/ip")
		.build();

	try (Response response = client.newCall(request).execute()) {
		String responseBody = response.body().string();
		System.out.println("代理出口IP:" + responseBody);
	}
}

如果代理生效,返回的 IP 应该是代理服务器的公网出口 IP,而不是本机公网 IP。

另一个测试坑

如果只是测试代理连通性,不建议用完整的 @SpringBootTest 启动整个 Spring 容器。

因为 Spring 测试上下文可能会先初始化数据库、Redis、定时任务等组件。比如本地 MySQL 没启动时,测试会先失败在数据库连接阶段,还没执行到 OkHttp 请求。

更轻量的方式是写一个普通 JUnit 测试,不加载 Spring 容器:

public class ProxyManagerTest {

	@Test
	public void testProxyIp() throws IOException {
		OkHttpClient client = ProxyManager.getProxyClient();

		Request request = new Request.Builder()
			.url("https://httpbin.org/ip")
			.build();

		try (Response response = client.newCall(request).execute()) {
			System.out.println(response.body().string());
		}
	}
}

总结

Java 使用 OkHttp 配置 SOCKS5 代理时,需要注意:

  • new Proxy(Proxy.Type.SOCKS, ...) 只配置代理地址。
  • SOCKS5 用户名密码不能只定义变量,必须通过 JDK Authenticator 注册。
  • OkHttp proxyAuthenticator 不适合处理 SOCKS5 认证。
  • Authenticator.setDefault(...) 是 JVM 全局配置,应按代理 host 和 port 做精确匹配。
  • 只测试网络代理时,尽量不要启动完整 Spring Boot 上下文。

这个问题的本质不是 OkHttp 没走代理,而是 SOCKS5 代理认证没有真正生效。

更多推荐