Loading

Spring Cloud Gateway 同时监听多个IP

背景

spring Cloud Gateway是Spring Cloud推出的第二代网关框架,取代Zuul网关。提供了路由转发、权限校验、限流控制等作用。Spring Cloud Gateway 使用非阻塞 API,支持 WebSockets。

我在使用 Spring Cloud Gateway 的时候遇到了一个问题,我们的系统上要求网关同时需要监听多个 IP 地址,但是 Spring Cloud Gateway 一个应用只允许监听一个 IP 地址.

通过 application.yml 可以为 Spring Cloud Gateway 设置 IP 地址:

server:
  port: 8080 # 设置端口号
  address: 192.168.31.11 # 设置监听的地址

其中 server.address 用于设置监听的本地 ip 地址,这里只能配置一个 ip 地址,如果配置同时多个 ip 地址会导致启动报错

server:
  port: 8080 # 设置端口号
  address: 192.168.31.11,192.168.44.12 # 设置监听的地址,错误格式
***************************
APPLICATION FAILED TO START
***************************

Description:

Failed to bind properties under 'server.address' to java.net.InetAddress:

    Property: server.address
    Value: "192.168.31.11,192.168.44.12"
    Origin: class path resource [application.yml] - 3:12
    Reason: failed to convert java.lang.String to java.net.InetAddress (caused by java.net.UnknownHostException: 不知道这样的主机。 (192.168.31.11,192.168.44.12))

这样配置会导致启动报错,那么 Spring Cloud Gateway 是否支持配置多个 IP 呢,答案是否定的.首先是无法通过 server.address 这个配置项配置同时监听多个 IP ,这个配置项在处理的时候就处理成了单个 IP 地址:

// org.springframework.boot.autoconfigure.web.ServerProperties
public class ServerProperties {
	private Integer port;
	private InetAddress address;
}

那么如何让 Spring Cloud Gateway 支持同时监听多个 IP 地址呢,这就需要从 Spring Cloud Gateway 底层进行分析并且手动做一些处理了,先上实现代码:

@Slf4j
@Configuration
@ConditionalOnProperty("server.other-address")
public class MultiServerAddrConfiguration {

    @Autowired
    private HttpHandler httpHandler;

    @Autowired
    private NettyReactiveWebServerFactory factory;

    // 设置监听多个地址
    @Value("${server.other-address}")
    private List<String> addressList;

    private List<WebServer> webServerList;

    @PostConstruct
    public void postConstruct() {
        webServerList = addressList.stream().map(addStr -> {
            try {
                return InetAddress.getByName(addStr);
            } catch (UnknownHostException e) {
                throw new RuntimeException(e);
            }
        }).map(address -> {
            InetAddress lastAddress = factory.getAddress();
            factory.setAddress(address);
            WebServer webServer = factory.getWebServer(httpHandler);
            webServer.start();
            log.info("Spring Cloud Gateway listened on {}", address);
            factory.setAddress(lastAddress);
            return webServer;
        }).toList();
    }

    @PreDestroy
    public void stop() {
        for (WebServer webServer : webServerList) {
            webServer.stop();
        }
    }
}

然后在配置中添加 other-address 的配置:

server:
  port: 8080 #端口号
  address: 192.168.31.227
  other-address: 192.168.191.2

此时我们再启动 Spring Cloud Gateway ,就可以看到已经监听了多个 IP 地址了:

img

实现原理

从代码上我们可以看到,我们实际上是劫持了 NettyReactiveWebServerFactory ,然后通过该 factory 创建了新的 WebServer,这些新的 WebServer 由于是通过我们自己来维护的,所以我们可以为其绑定我们想要的 IP 地址.

由于我们是劫持了 NettyReactiveWebServerFactory, 因此我们创建的 WebServer 要早于 Spring Cloud GateWay 创建的 WebServer 启动。

至于实现过程,是参考了 Spring Cloud Gateway 启动的最后一步,创建 WebServer 的过程:

// org.springframework.boot.web.reactive.context.WebServerManager#WebServerManager
private final DelayedInitializationHttpHandler handler;

private final WebServer webServer;

WebServerManager(ReactiveWebServerApplicationContext applicationContext, ReactiveWebServerFactory factory,
        Supplier<HttpHandler> handlerSupplier, boolean lazyInit) {
    this.applicationContext = applicationContext;
    Assert.notNull(factory, "Factory must not be null");
    this.handler = new DelayedInitializationHttpHandler(handlerSupplier, lazyInit);
    this.webServer = factory.getWebServer(this.handler);
}

void start() {
    this.handler.initializeHandler();
    this.webServer.start();
    this.applicationContext.publishEvent(new ReactiveWebServerInitializedEvent(this.webServer, this.applicationContext));
}

其实这里也是无奈之举,因为 NettyServer 本身也只支持同时监听一个 IP 地址. Spring Cloud Gateway 在设计上也遵照了 NettyServer 的设计思路(出于安全考虑?).

// org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory#createHttpServer
private HttpServer createHttpServer() {
    HttpServer server = HttpServer.create();
    if (this.resourceFactory != null) {
        LoopResources resources = this.resourceFactory.getLoopResources();
        Assert.notNull(resources, "No LoopResources: is ReactorResourceFactory not initialized yet?");
        server = server.runOn(resources).bindAddress(this::getListenAddress);
    } else {
        // 只绑定了一个地址
        server = server.bindAddress(this::getListenAddress);
    }
    if (getSsl() != null && getSsl().isEnabled()) {
        // 配置 ssl
        server = customizeSslConfiguration(server);
    }
    if (getCompression() != null && getCompression().getEnabled()) {
        CompressionCustomizer compressionCustomizer = new CompressionCustomizer(getCompression());
        server = compressionCustomizer.apply(server);
    }
    server = server.protocol(listProtocols()).forwarded(this.useForwardHeaders);
    return applyCustomizers(server);
}

配置 监听 http 80 端口并重定向到 https 443 端口

由于基本实现思路都是通过创建自己维护 WebServer 的方式,因此我们还可以配置监听多个端口,比如这里通过监听 80 端口,并将请求重定向到 443 端口上去:

@Configuration
public class HttpToHttpsRedirectConfiguration {

    @Value("${server.port}")
    private Integer serverSSLPort;

    private WebServer webServer;

    @PostConstruct
    public void startRedirectServer() {
        NettyReactiveWebServerFactory httpNettyReactiveWebServerFactory = new NettyReactiveWebServerFactory(80);
        webServer = httpNettyReactiveWebServerFactory.getWebServer((request, response) -> {
            URI uri = request.getURI();
            URI httpsUri;
            try {
                httpsUri = new URI("https", uri.getUserInfo(), uri.getHost(), serverSSLPort, uri.getPath(), uri.getQuery(), uri.getFragment());
            } catch (URISyntaxException e) {
                return Mono.error(e);
            }
            response.setStatusCode(HttpStatus.MOVED_PERMANENTLY);
            response.getHeaders().setLocation(httpsUri);
            return response.setComplete();
        });
        webServer.start();
    }


    @PreDestroy
    public void stop(){
        webServer.stop()
    }
}

参考资料

posted @ 2024-07-31 23:15  ghimi  阅读(353)  评论(0)    收藏  举报