相关推荐recommended
Spring Cloud Gateway网关转发websocket服务配置
作者:mmseoamin日期:2024-02-03

Spring Cloud Gateway网关是所有微服务的统一入口。

1、Spring Cloud Gateway关键术语

  • Route:路由,网关配置的基本组成模块。一个Route模块由一个 ID,一个目标 URI,一组断言和一组过滤器定义。如果断言为真,则路由匹配,目标URI会被访问。
  • Predicate:断言,可以使用它来匹配来自 HTTP 请求的任何内容。
  • Filter:过滤器,可以使用它拦截和修改请求,并且对上游的响应,进行二次处理。过滤器org.springframework.cloud.gateway.filter.GatewayFilter类的实例。

    2、Spring Cloud Gateway处理流程

    客户端向 Spring Cloud Gateway 发出请求。然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(“pre”)或之后(“post”)执行业务逻辑。

    Spring Cloud Gateway网关转发websocket服务配置,在这里插入图片描述,第1张

    3、在Spring Cloud Gateway网关中,在yml配置文件中通过如下配置对websocket请求进行转发:

    spring:
      cloud:
        gateway:
          routes:
            - id: websocket1
              uri: lb:ws://serviceName #使用方式2:websocket配置,通过nacos注册中心调用serviceName
              predicates: 
                - Path=/websocket
    

    当websocket服务为基于netty的socketio,netty需要单独开端口访问,上面方式要直接指定websocket服务的端口,多个websocket服务时,可以配置多个相同的路由规则,每个指定一个socketio服务,然后通权重实现负载均衡:

    spring:
      cloud:
        gateway:
          routes:
            - id: websocket1
              uri: ws://127.0.0.1:8081
              predicates:
                - Path=/socket
                - Weight=group1,50
            - id: websocket2
              uri: ws://127.0.0.1:8082
              predicates:
                - Path=/socket
                - Weight=group1,50
    

    4、在Spring Cloud Gateway网关中转发websocket请求时,连上websocket后出现立马断开问题,报错java.lang.UnsupportedOperationException,详细如下:

    2023-10-24 10:05:23.433 ERROR 12636 --- [ctor-http-nio-6] o.s.w.s.adapter.HttpWebHandlerAdapter    : [6726d297-6] Error [java.lang.UnsupportedOperationException] for HTTP GET "/socket/?EIO=3&transport=websocket", but ServerHttpResponse already committed (200 OK)
    2023-10-24 10:05:23.433 ERROR 12636 --- [ctor-http-nio-6] r.n.http.server.HttpServerOperations     : [6726d297-1, L:/192.168.20.5:9099 - R:/192.168.20.5:9099] Error finishing response. Closing connection
    java.lang.UnsupportedOperationException: null
    	at org.springframework.http.ReadOnlyHttpHeaders.put(ReadOnlyHttpHeaders.java:126) ~[spring-web-5.3.20.jar:5.3.20]
    	Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
    Error has been observed at the following site(s):
    	*__checkpoint ⇢ org.springframework.web.cors.reactive.CorsWebFilter [DefaultWebFilterChain]
    	*__checkpoint ⇢ org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]
    	
    	*__checkpoint ⇢ HTTP GET "/socket/?EIO=3&transport=websocket" [ExceptionHandlingWebHandler]
    Original Stack Trace:
    		at org.springframework.http.ReadOnlyHttpHeaders.put(ReadOnlyHttpHeaders.java:126) ~[spring-web-5.3.20.jar:5.3.20]
    

    通过分析发现是Gateway处理跨域时使用的是如下方式:

            // 跨域配置源
            UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            //设置跨域的配置信息
            CorsConfiguration corsConfiguration = new CorsConfiguration();
            // 允许所有请求来源进行跨域
            //corsConfiguration.addAllowedOrigin("*");
            ...
    

    需改成reactor响应式方式,如下:

    return (ServerWebExchange ctx, WebFilterChain chain) -> {
                ServerHttpRequest request = ctx.getRequest();
                // 使用SpringMvc自带的跨域检测工具类判断当前请求是否跨域
                if (!CorsUtils.isCorsRequest(request)) {
                    return chain.filter(ctx);
                }
                HttpHeaders requestHeaders = request.getHeaders();                                  // 获取请求头
                ServerHttpResponse response = ctx.getResponse();                                    // 获取响应对象
                HttpMethod requestMethod = requestHeaders.getAccessControlRequestMethod();          // 获取请求方式对象
                HttpHeaders headers = response.getHeaders();                                        // 获取响应头
                headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, requestHeaders.getOrigin());   // 把请求头中的请求源(协议+ip+端口)添加到响应头中(相当于yml中的allowedOrigins)
                headers.addAll(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders.getAccessControlRequestHeaders());
                if (requestMethod != null) {
                    headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, requestMethod.name());    // 允许被响应的方法(GET/POST等,相当于yml中的allowedMethods)
                }
                headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");       // 允许在请求中携带cookie(相当于yml中的allowCredentials)
                headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "*");             // 允许在请求中携带的头信息(相当于yml中的allowedHeaders)
                headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "18000L");               // 本次跨域检测的有效期(单位毫秒,相当于yml中的maxAge)
                if (request.getMethod() == HttpMethod.OPTIONS) {                                    // 直接给option请求反回结果
                    response.setStatusCode(HttpStatus.OK);
                    return Mono.empty();
                }
                return chain.filter(ctx);            // 不是option请求则放行
            };
    

    5、Spring Cloud Gateway处理跨域,可以通过yml配置方式实现,如:

        gateway:
          # 全局的跨域配置
          globalcors:
            # 解决options请求被拦截问题
            add-to-simple-url-handler-mapping: true
            cors-configurations:
              # 拦截的请求
              '[/**]':
                # 允许跨域的请求
                #allowedOrigins: "*" # spring boot2.4以前的配置
                allowedOriginPatterns: "*" # spring boot2.4以后的配置
                # 允许请求中携带的头信息
                allowedHeaders: "*"
                # 运行跨域的请求方式
                allowedMethods: "*"
                # 是否允许携带cookie
                allowCredentials: true
                # 跨域检测的有效期,单位s
                maxAge: 3600
    

    也可以通过编码的方式定义跨域配置类,如:

    @Configuration
    public class CorsConfig {
        @Bean
        public WebFilter corsFilter() {
            return (ServerWebExchange ctx, WebFilterChain chain) -> {
                ServerHttpRequest request = ctx.getRequest();
                // 使用SpringMvc自带的跨域检测工具类判断当前请求是否跨域
                if (!CorsUtils.isCorsRequest(request)) {
                    return chain.filter(ctx);
                }
                HttpHeaders requestHeaders = request.getHeaders();                                  // 获取请求头
                ServerHttpResponse response = ctx.getResponse();                                    // 获取响应对象
                HttpMethod requestMethod = requestHeaders.getAccessControlRequestMethod();          // 获取请求方式对象
                HttpHeaders headers = response.getHeaders();                                        // 获取响应头
                headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, requestHeaders.getOrigin());   // 把请求头中的请求源(协议+ip+端口)添加到响应头中(相当于yml中的allowedOrigins)
                headers.addAll(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders.getAccessControlRequestHeaders());
                if (requestMethod != null) {
                    headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, requestMethod.name());    // 允许被响应的方法(GET/POST等,相当于yml中的allowedMethods)
                }
                headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");       // 允许在请求中携带cookie(相当于yml中的allowCredentials)
                headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "*");             // 允许在请求中携带的头信息(相当于yml中的allowedHeaders)
                headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "18000L");               // 本次跨域检测的有效期(单位毫秒,相当于yml中的maxAge)
                if (request.getMethod() == HttpMethod.OPTIONS) {                                    // 直接给option请求反回结果
                    response.setStatusCode(HttpStatus.OK);
                    return Mono.empty();
                }
                return chain.filter(ctx);            // 不是option请求则放行
            };
        }
    }