如何为Spring WebFlux WebClient配置无需CONNECT隧道的HTTP正向代理?
如何为Spring WebFlux WebClient配置无需CONNECT隧道的HTTP正向代理?
嗨,我完全懂你现在的困扰——Netty HttpClient不管目标是HTTP还是HTTPS,都会默认用CONNECT方法去和代理建立隧道,可很多普通的正向代理根本不支持这一套,直接给你返回403,确实头疼。下面给你两个靠谱的解决方案,你可以根据自己的情况选:
方案一:切换到Jetty HttpClient(最省心的选择)
Jetty的HttpClient原生支持普通的HTTP正向代理模式,不会强制用CONNECT隧道,完美适配你的场景,而且配置起来也很简单。
1. 替换依赖
先把Spring WebFlux默认的Netty依赖替换成Jetty的:
Maven(pom.xml)
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> <!-- 排除默认的Netty依赖 --> <exclusions> <exclusion> <groupId>io.projectreactor.netty</groupId> <artifactId>reactor-netty-http</artifactId> </exclusion> </exclusions> </dependency> <!-- 引入Jetty相关依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jetty</artifactId> </dependency> <dependency> <groupId>org.springframework.http</groupId> <artifactId>spring-http-client-jetty</artifactId> </dependency>
Gradle(build.gradle)
implementation('org.springframework.boot:spring-boot-starter-webflux') { exclude group: 'io.projectreactor.netty', module: 'reactor-netty-http' } implementation 'org.springframework.boot:spring-boot-starter-jetty' implementation 'org.springframework.http:spring-http-client-jetty'
2. 配置Jetty HttpClient和WebClient
替换你原来的HttpClient Bean为Jetty的实现,同时保留你原来的代理配置逻辑:
import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.ProxyConfiguration; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.http.client.JettyClientHttpConnector; import org.springframework.web.reactive.function.client.WebClient; import lombok.Data; @Data @Configuration @ConfigurationProperties(prefix = "proxy") public class ProxyConfiguration { private boolean enabled = false; private HttpProxy http = new HttpProxy(); @Data public static class HttpProxy { private String host = "localhost"; private Integer port = 8080; private String noProxy = "localhost|127.0.0.1"; private HttpProxyAuthentication authentication = new HttpProxyAuthentication(); @Data public static class HttpProxyAuthentication { private boolean enabled = false; private String username; private String password; } } @Bean @Primary public HttpClient jettyHttpClient() throws Exception { HttpClient httpClient = new HttpClient(new SslContextFactory.Client()); httpClient.setFollowRedirects(true); if (this.isEnabled()) { ProxyConfiguration proxyConfig = httpClient.getProxyConfiguration(); // 添加HTTP代理配置 ProxyConfiguration.Proxy proxy = proxyConfig.addProxy( ProxyConfiguration.Proxy.Type.HTTP, new java.net.InetSocketAddress(http.getHost(), http.getPort()) ); // 配置无需代理的主机 String[] noProxyHosts = http.getNoProxy().split("\\|"); for (String host : noProxyHosts) { proxyConfig.getExcludedAddresses().add(host.trim()); } // 添加代理认证(如果需要) if (http.getAuthentication().isEnabled() && http.getAuthentication().getUsername() != null) { proxy.getAuthentication().addBasicAuthentication( http.getAuthentication().getUsername(), http.getAuthentication().getPassword() ); } } else { // 不启用代理时使用系统代理属性 proxyConfig.getProxies().addAll(ProxyConfiguration.load()); } httpClient.start(); return httpClient; } @Bean public WebClient eebClient(HttpClient jettyHttpClient) { return WebClient.builder() .baseUrl(BASE_URL) // 替换成你的实际基础URL .clientConnector(new JettyClientHttpConnector(jettyHttpClient)) .build(); } }
这样配置后,Jetty会自动对HTTP请求使用普通正向代理模式(直接把完整URL发给代理,不用CONNECT),HTTPS请求还是会用CONNECT(这是HTTPS协议的必然要求,没法绕开),完全符合你的需求。
方案二:自定义Netty请求逻辑(适合不想换客户端的场景)
如果你不想替换Netty,也可以通过自定义请求过滤器,手动把HTTP请求改成普通正向代理的格式,绕过Netty的CONNECT逻辑:
import io.netty.handler.codec.http.HttpHeaders; import reactor.netty.http.client.HttpClient; import reactor.netty.resolver.DefaultAddressResolverGroup; import reactor.netty.transport.AddressUtils; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.web.reactive.function.client.WebClient; @Data @Configuration @ConfigurationProperties(prefix = "proxy") public class ProxyConfiguration { private boolean enabled = false; private HttpProxy http = new HttpProxy(); @Data public static class HttpProxy { private String host = "localhost"; private Integer port = 8080; private String noProxy = "localhost|127.0.0.1"; private HttpProxyAuthentication authentication = new HttpProxyAuthentication(); @Data public static class HttpProxyAuthentication { private boolean enabled = false; private String username; private String password; } } @Bean @Primary public HttpClient httpClient() { HttpClient httpClient = HttpClient.create() .resolver(DefaultAddressResolverGroup.INSTANCE) .followRedirect(true); if (this.isEnabled()) { HttpProxy proxy = this.getHttp(); httpClient = httpClient // 设置代理地址为默认远程地址 .remoteAddress(() -> AddressUtils.createUnresolved(proxy.getHost(), proxy.getPort())) // 自定义请求逻辑,处理HTTP正向代理 .doOnRequest((request, connection) -> { String targetUri = request.uri().toString(); // 仅处理HTTP目标(HTTPS必须用CONNECT,没法绕开) if (request.uri().getScheme().equals("http")) { // 把完整目标URL作为请求路径 request.uri(targetUri); // 添加代理认证头(如果需要) if (proxy.getAuthentication().isEnabled() && proxy.getAuthentication().getUsername() != null) { String auth = proxy.getAuthentication().getUsername() + ":" + proxy.getAuthentication().getPassword(); String base64Auth = java.util.Base64.getEncoder().encodeToString(auth.getBytes()); request.header(HttpHeaders.Names.PROXY_AUTHORIZATION, "Basic " + base64Auth); } } }) // 处理无需代理的主机 .filter((request, next) -> { String host = request.uri().getHost(); if (proxy.getNoProxy().contains(host)) { // 直接请求目标地址,不走代理 return next.request( request.remoteAddress(() -> AddressUtils.createUnresolved(host, request.uri().getPort())) ); } return next.request(request); }); } else { httpClient = httpClient.proxyWithSystemProperties(); } return httpClient; } @Bean public WebClient eebClient(HttpClient httpClient) { return WebClient.builder() .baseUrl(BASE_URL) // 替换成你的实际基础URL .clientConnector(new ReactorClientHttpConnector(httpClient)) .build(); } }
注意:这个方案只适用于目标是HTTP的场景,HTTPS请求必须用CONNECT隧道,这是HTTPS协议的硬性要求,没法绕开。另外这个方案需要自己处理很多细节,比如代理认证、无需代理主机的匹配,稳定性不如Jetty的原生支持,所以更推荐方案一。
总结
如果你想快速解决问题,方案一(切换到Jetty)是最稳妥的选择,因为Jetty原生支持普通正向代理,不用自己造轮子;如果实在不想替换客户端,方案二也能凑合用,但需要多做一些兼容性测试。




