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 地址了:

实现原理
从代码上我们可以看到,我们实际上是劫持了 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()
}
}
参考资料
本文来自博客园,作者:ghimi,转载请注明原文链接:https://www.cnblogs.com/ghimi/p/18335720

浙公网安备 33010602011771号