先贴上解决方案吧,一下简称Spring cloud gateway 为SCG

server:
  port: 8000

spring:
  cloud:
    gateway:
      routes:
      - id: https
        uri: https://www.zhaoxu4java.com/-/x/pro/market/overview
        predicates:
        - Path=/overview
      - id: wss
        uri: wss://www.zhaoxu4java.com:443/-/s/pro/ws
        filters:
        - RemoveRequestHeader=Host
        - AddRequestHeader=Host, www.zhaoxu4java.com
        predicates:
        - Path=/ws

第一坑 端口之坑:

HTTPS或者HTTP协议可以不用设置端口号,默认443或者80,参见Spring cloud gateway-core源码

@ConfigurationProperties("spring.cloud.gateway.x-forwarded")
public class XForwardedHeadersFilter implements HttpHeadersFilter, Ordered {
	/** default http port */
	public static final int HTTP_PORT = 80;

	/** default https port */
	public static final int HTTPS_PORT = 443;

	/** http url scheme */
	public static final String HTTP_SCHEME = "http";

	/** https url scheme */
	public static final String HTTPS_SCHEME = "https";

但是对于wss或者ws并没有默认的端口号配置,所以如果在配置websocket 服务时域名后没有添加端口号,SCG会很贴心的给你添加上一个,把server port 作为该服务的端口,除非你的ws服务后面的端口号真的和server port 一致,否则请添加端口号,参见SCG-core 源码:

package org.springframework.cloud.gateway.filter;
public class RouteToRequestUrlFilter implements GlobalFilter, Ordered {
@Override
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //省略
		...
		URI requestUrl = UriComponentsBuilder.fromUri(uri)
				.uri(routeUri)
				.build(encoded)
				.toUri();
		exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
		return chain.filter(exchange);
}
}

这块儿代码即为SCG 生成请求url代码,有兴趣可以点开toUri方法即可看到具体的指定port逻辑。另外SCG对于WebSocket 的另外一种支持形式可以让你避免添加端口号,即将scheme设置为http或https而请求SCG 的客户端为websocket请求形式即可,代码如下:

- id: wss
        uri: https://www.zhaoxu4java.com/-/s/pro/ws
        filters:
        - RemoveRequestHeader=Host
        - AddRequestHeader=Host, www.zhaoxu4java.com
        predicates:
        - Path=/ws

客户端:

HttpClient httpClient = vertx.createHttpClient(new HttpClientOptions().setSsl(port == 443).setConnectTimeout(timeout));

        httpClient.websocket(port, host, requestUrl, websocket -> {...}

只要客户端是ws请求,会在请求的headers中添加upgrade:websocket,SCG会对此参数进行检测,并最终将requestUrl的scheme换成https或http,源码如下:

package org.springframework.cloud.gateway.filter;

public class WebsocketRoutingFilter implements GlobalFilter, Ordered {
...
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
		changeSchemeIfIsWebSocketUpgrade(exchange);
        ...
}


private void changeSchemeIfIsWebSocketUpgrade(ServerWebExchange exchange) {
		// Check the Upgrade
		URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);
		String scheme = requestUrl.getScheme().toLowerCase();
		String upgrade = exchange.getRequest().getHeaders().getUpgrade();
		// change the scheme if the socket client send a "http" or "https"
		if ("WebSocket".equalsIgnoreCase(upgrade) && ("http".equals(scheme) || "https".equals(scheme))) {
			String wsScheme = convertHttpToWs(scheme);
			URI wsRequestUrl = UriComponentsBuilder.fromUri(requestUrl).scheme(wsScheme).build().toUri();
			exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, wsRequestUrl);
			if (log.isTraceEnabled()) {
				log.trace("changeSchemeTo:[" + wsRequestUrl + "]");
			}
		}
	}

	/* for testing */ static String convertHttpToWs(String scheme) {
		scheme = scheme.toLowerCase();
		return "http".equals(scheme) ? "ws" : "https".equals(scheme) ? "wss" : scheme;
	}
}

我自己感觉这样的写法会很奇怪,我更愿意参考XForwardedHeadersFilter 类的实现方式添加对ws和wss的支持,但是从另外一方面讲,websocket本身就是基于http一次握手的,所以有了这段changeSchemeIfIsWebSocketUpgrade进行协议升级吧,参见菜鸟教程(

WebSocket 实例

WebSocket 协议本质上是一个基于 TCP 的协议。

为了建立一个 WebSocket 连接,客户端浏览器首先要向服务器发起一个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了一些附加头信息,其中附加头信息"Upgrade: WebSocket"表明这是一个申请协议升级的 HTTP 请求,服务器端解析这些附加的头信息然后产生应答信息返回给客户端,客户端和服务器端的 WebSocket 连接就建立起来了,双方就可以通过这个连接通道自由的传递信息,并且这个连接会持续存在直到客户端或者服务器端的某一方主动的关闭连接。

总之这一点,仁者见仁智者见智吧

第二坑 导致404/403 错误码的Host 参数!

先介绍一下远端环境,SCG代理的域名对应的是一个nginx服务,nginx服务继续转发后面真正的server,在本地搭建该环境时一切正常,而代理线上和测试域名甚至是ip时出现403/404错误,debug发现在发送websocket请求时host仍然为localhost而不是requestUrl对应的host,因此通过配置filters参数进行修正,测试通过。也试图通过自定义filter进行更改也是可行的,可参考配置的filters对应的filterFactory中的代码进行headers 的添加和删除。当然在集成了服务发现Eureka后第二种方式应该是更好的选择,至于为什么必须保证host 和requestUrlhost一致这个问题,我需要继续研究一下,另外补充一下只有ws服务才会出现这种情况,http Host没有影响