You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何为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原生支持普通正向代理,不用自己造轮子;如果实在不想替换客户端,方案二也能凑合用,但需要多做一些兼容性测试。

火山引擎 最新活动