SpringCloud实践:Gateway网关
2024年01月08日
107
网关服务网关是跨一个或多个服务节点提供单个统一的访问入口下图是API网关的**位置、功能**2、Gateway与Zuul比较Spring Cloud Gateway 作为 Spring Cloud框架的第二代网关,在功能上要比 Zuul更加的强大,性能也更好。Spring Cloud Gateway替换掉 Zuul的成本上是非常低的,几乎可以无缝切换。Spring Cloud Gateway几乎包含了 Zuul的所有功能。

一、概述

1、网关

服务网关是跨一个或多个服务节点提供单个统一的访问入口

下图是API网关的**位置功能**

image.png

2、Gateway与Zuul比较

Spring Cloud Gateway 作为 Spring Cloud框架的第二代网关,在功能上要比 Zuul更加的强大,性能也更好。Spring Cloud Gateway替换掉 Zuul的成本上是非常低的,几乎可以无缝切换。Spring Cloud Gateway几乎包含了 Zuul的所有功能。

  • 优缺点比较


优点缺点
Gateway1、线程开销小
2、使用轻量级 Netty 异步IO实现通信
3、支持各种长连接,WebSocket
4、Spring 官方推荐,重点支持,功能较 Zuul更丰富,支持限流监控等
1、源码复杂
2、运维复杂
3、目前资料、实践较少,出现问题排查难度大
Zuul1、编码模型简单
2、开发调试运维简单
1、线程上下文切换开销
2、线程数限制
3、延迟堵塞耗尽线程链接资源
  • 性能比较

    **Zuul 与 Gateway 压测结果:**休眠时间模仿后端请求时间,线程数 2000,请求时间 360秒=6分钟。配置情况:Gateway 默认配置,Zuul网关的 Tomcat 最大线程数为 400,hystrix 超时时间为 100000。

    休眠时间测试样本,单位=个Zuul/Gateway平均响应时间,单位=毫秒Zuul/Gateway99%响应时间,单位=毫秒 Zuul/Gateway错误次数,单位=个Zuul/Gateway错误比例Zuul/Gateway
    休眠100ms294134/10593212026/5466136/1774104/00.04%/0%
    休眠300ms101194/3999095595/148915056/16901114/01.10%/0%
    休眠600ms51732/20126211768/297527217/32032476/04.79%/0%
    休眠1000ms31896/12095619359/491446259/51153598/011.28%/0%

    **测试结果:**Gateway在高并发和后端服务响应慢的场景下比 Zuul的表现要好

3、GateWay 架构图

image.png

【过程】

  1. GatewayClient 向 GatewayServer 发出请求

  2. 请求首先会被 HttpWebHandlerAdapter 进行提取组转成网关上下文

  3. 然后网关的上下文会传递到 DispatcherHandler,它负责将请求分发给 RoutePredicateHandlerMapping

  4. RoutePredicateHandlerMapping负责路由查找,并更具路由断言判断路由是否可用

  5. 如果断言成功,由 FilteringWebHandler 创建过滤器链并调用

  6. 请求会一次经过 PreFilter —> 微服务 —> PostFilter 的方法,最终返回响应

image.png

二、快速使用

搭建一个gateway服务,并注册到nacos中

  • 依赖

    <!-- gateway网关 --><dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId></dependency><!-- gateway网关(用于实现全局异常捕获) --><dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-gateway-adapter</artifactId></dependency><!-- 端点监控 --><dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId></dependency><!-- nacos注册中心 --><dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency>

    📢:不要依赖spring-boot-starter-web,因为web基于springmvc,springmvc运行的服务器是tomcat,gateway基于netty

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId></dependency>
  • 配置文件

    server:
    	port: 8040 # gateway的端口号
    	spring:
    	application: 
    		name: cloud-gateway # 服务名
    	cloud: 
    		# nacos配置
    		nacos:	
    			discovery:
    				server-addr: 127.0.0.1:8848
    				username: nacos        password: nacos
    		gateway:
    			# gateway配置
    			 routes:         - id: cloud-goods  # cloud-goods服务
          uri: lb://cloud-goods/ # cloud-goods服务转发到cloud-goods服务(也可以http://localhost:8081)
              predicates:            - Path=/goods/** # 只有/goods/**的接口可被访问
  • 启动类

  • @SpringBootApplication
    // 启动类上开启服务注册与发现功能
    @EnableEurekaClientpublic class ApiGatewayApp {    public static void main(String[] args) {
            SpringApplication.run(ApiGatewayApp.class, args);
        }
    }


三、配置详解

3.1、基本配置

server:
	port: 8040 # gateway的端口号
	spring:
	application: 
		name: cloud-gateway # 服务名
	cloud: 
		# nacos配置
		nacos:	
			discovery:
				server-addr: 127.0.0.1:8848
				username: nacos        password: nacos
		gateway:
			# gateway配置      routes:         - id: cloud-goods  
			# 名服务          uri: lb://cloud-goods/ 
			# 转发到cloud-goods服务(也可以http://localhost:8081)          predicates:            - Path=/goods/** 
			# 只有/goods/**的接口可被访问        - id: baidu           uri: https://www.baidu.com          predicates:            - Path=/**
  • id: 路由的唯一标识,区别于其他Route

  • uri: 路由指向目的地 uri,即客户端请求最终被转发到的微服务

  • order: 用于多个 Route 之间的排序,数值越小排序越靠前,匹配优先级越高

  • predicate: 用来进行条件判断,只有断言都返回真,才会真正的执行路由

  • filter: 过滤器用于修改请求和响应信息

3.2、路由转发

3.2.1、静态路由

uri路由直接写原始url

server:  port: 8000spring:  application:    name: api-gateway-server  cloud: 
#网关配置    gateway: 
#路由配置: 转发规则      routes:  
#集合。      - id: gateway-provider  
#ID:唯一标示,默认是一个UUID        uri: http://localhost:8001/  #uri:转发路径        predicates:  
#predicates: 条件,用于请求网关路径的匹配规则          - path=/goods/**

3.2.2、动态路由

引入nacos注册中心,使用nacos中的服务名

server:
	port: 8000 # gateway的端口号
	spring:
	application: 
		name: cloud-gateway # 服务名
	cloud: 
		# nacos配置
		nacos:	
			discovery:
				server-addr: 127.0.0.1:8848
				username: nacos        password: nacos
		gateway:
			# gateway配置      routes:         - id: cloud-goods  
			# 名服务          uri: lb://cloud-goods/ 
			# 转发到cloud-goods服务          predicates:            - Path=/goods/** 
			# 只有/goods/**的接口可被访问

3.3、断言

pre:在转发之前执行,可以做参数校验、权限校验、流量监控、日志输出、协议转换等。

3.3.1、内置断言

image.png

predicates: 
	- Path=/api/**
	# 时间校验
	- After=2022-02-10T16:45:10.259+08:00[Asia/Shanghai] # 在这个时间点之后可以访问
	- Before=2022-02-10T16:45:10.259+08:00[Asia/Shanghai] # 在这个时间点之前可以访问
	# Cookie校验
	- Cookie=age,18 #age是cookie名,18是cookie值,携带该cookie才能访问
	# Header校验
	- Header=token,123 # 携带该Header才能访问
	# Host校验
	- Host=**.baidu.com
	# Method方法校验
	- Method=GET
	# Query参数校验
	- Query=baz,123
	# RemoteAddr地址校验
	- RemoteAddr=192.168.234.122,192.168.234.123

3.3.2、自定义断言

  • MyConfig

    @Datapublic class MyConfig {  private String key;  private String value;
    }
  • RoutePredicateFactory

    /**
     * 自定义断言工厂类
     * 
     * 1、继承AbstractRoutePredicateFactory类
     * 2、重写apply方法
     * 3、apply方法的参数是自定义的配置类,可以在apply方法中直接获取使用配置参数。
     * 4、类的命名需要以RoutePredicateFactory结尾
     * 
     */@Componentpublic class MyRoutePredicateFactory extends AbstractRoutePredicateFactory<MyConfig> {  public MyRoutePredicateFactory() {        super(MyConfig.class);
      }  
      @Override
      public Predicate<ServerWebExchange> apply(MyConfig config) {    return new Predicate<ServerWebExchange>() {            @Override
                public boolean test(ServerWebExchange serverWebExchange) {                // 从请求头获取key对应的value
                		String value = serverWebExchange.getRequest().getHeaders().getFirst(config.getKey());   
                    // 判断value会否相等
                    if (StringUtils.equals(config.getValue(), value)) {                  return true;
                    }                return false;
                }
            };
      }  
      // 指定切分方式
      @Override
      public List<String> shortcutFieldOrder() {    return Arrays.asList("key", "value");
      }
    }
  • 使用

    predicates: 
    	- Path=/api/**
    	# 自定义校验
    	- MyHeader=name,tom

3.4、过滤器

在 Gateway 的过滤器中又可以分为 局部过滤器 和 全局过滤器局部 是用于某一个路由上的,全局 是用于所有路由上的。不管是 局部 还是 全局,生命周期都分为 pre 和 post

  • pre: 作用于路由到微服务之前调用。可以做参数校验、权限校验、流量监控、日志输出、协议转换等。

  • post: 作用于路由到微服务之后执行。可以做响应内容、响应头的修改、日志的输出、流量监控等。

3.4.1、内置过滤器

3.4.1.1、常见过滤器

Spring Cloud Gateway实战之五:内置filter

  • 常见局部过滤器

    我们选几种常用的过滤器进行说明:(下列过滤器省略后缀 GaewayFilterFactory,完整名称为 前缀+后缀)

    过滤器前缀作用参数
    StripPrefix用于截断原始请求的路径使用数字表示要截断的路径数量
    AddRequestHeader为原始请求添加 HeaderHeader 的名称及值
    AddRequestParameter为原始请求添加请求参数参数名称及值
    Retry针对不同的响应进行重试reties、statuses、methods、series
    RequestSize设置允许接收最大请求包的大小请求包大小,单位字节,默认5M
    SetPath修改原始请求的路径修改后的路径
    RewritePath重写原始的请求路径原始路径正则表达式以及重写后路径的正则表达式
    PrefixPath为原始请求路径添加前缀前缀路径
    RequestRateLimiter对请求限流,限流算法为令牌桶KeyResolver、reteLimiter、statusCode、denyEmptyKey
  • 常见全局过滤器

    全局过滤器作用于所有路由,无需配置。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能

    相对于局部过滤器,全局过滤器的命名就没有太多约束了,毕竟不需要在配置文件中进行配置。

    过滤器名称作用
    ForwardPathFilter / ForwardRoutingFilter路径转发相关过滤器
    LoadBalanceerClientFilter负载均衡客户端相关过滤器
    NettyRoutingFilter / NettyWriteResponseFilterHttp 客户端相关过滤器
    RouteToRequestUrlFilter路由 URL 相关过滤器
    WebClientHttpRoutingFilter / WebClientWriteResponseFilter请求 WebClient 客户端转发请求真实的URL并将响应写入到当前的请求响应中
    WebsocketRoutingFilterwebsocket 相关过滤器

3.4.1.2、过滤器使用

gateway:
	routes:
		predicates: 
			- Path=/api/**
		filters:
			- AddRequestHeader=token,123 # 往header里面放k-v
@GetMapping("/findById/{id}")public Goods findById(@PathVarible String id, @RequestHeader("token") String token) {
  System.out.println(token); // token123
  return null;
}

3.4.2、自定义过滤器

3.4.2.1、局部过滤器

  • MyConfig

    @Datapublic class MyConfig {  private String key;  private String value;
    }
  • MyGatewayFilterFactory

    /**
     * 自定义过滤器:计算接口执行时间
     * 
     * 1、继承AbstractGatewayFilterFactory类
     * 2、重写apply方法
     * 3、apply方法的参数是自定义的配置类,可以在apply方法中直接获取使用配置参数。
     * 4、类的命名需要以GatewayFilterFactory结尾
     * 
     */@Componentpublic class MyGatewayFilterFactory extends AbstractGatewayFilterFactory<MyConfig> {  public MyGatewayFilterFactory {        super(MyConfig.class);
      }  
      @Override
      public GatewayFilter apply(MyConfig config) {    return new GatewayFilter() {            @Override
                public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {                // 记录开启时间
                    long startTime = System.currentTimeMillis();
                  	System.out.println("key:" + config.getKey());
                  	System.out.println("value:" + config.getValue());                // return chain.filter(exchange); // 放行
                    return chain.filter(exchange).then(                  	// filter后处理
                    	  Mono.formRunnable(() -> {                    	long endTime = System.currentTimeMillis();
                          System.out.println("total: " + (endTime - startTime));
                        })
                    );
                }
            };
      }  
      // 指定切分方式
      @Override
      public List<String> shortcutFieldOrder() {    return Arrays.asList("key", "value");
      }
    }
  • 使用

    gateway:
    	routes:
    		predicates: 
    			- Path=/api/**
    		filters:
    			- My=a,b

3.4.2.2、全局过滤器

【方式一】

AuthFilter

/**
 * 全局过滤器:实现token认证
 * 
 * 1、实现GlobalFilter、Ordered接口
 * 2、实现filter、getOrder方法
 * 3、apply方法的参数是自定义的配置类,可以在apply方法中直接获取使用配置参数。
 * 4、类的命名需要以GatewayFilterFactory结尾
 * 
 */@Componetpublic class AuthFilter implements GlobalFilter, Ordered {  	// 针对所有的路由进行过滤
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {      	// 1、获取请求参数
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        String token = request.getHeaders().getFirst("token");        // 2、校验
        if ("myToken".equals(token)) {            // 3、放行
          	return chan.filter(exchange);
        }        // 4、拦截:禁止访问返回状态码
      	exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);        return exchange.getResponse().setComplete();
    }  
  	// 返回的数字越小,越先执行
    @Override
    public int getOrder() {        return 0;
    }
}

配置完直接生效

【方式二】

/**
 * 全局过滤器方式二:实现token认证
 * 
 * 1、直接在启动类里写,可以写多个
 * 2、加@Order注解用于排序类似于实现Ordered接口并实现getOrder()方法
 *
 */@Bean@Order(1)public GlobalFilter AuthFilter() {    return (exchange, chain) -> {        // 1、获取请求参数
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        String token = request.getHeaders().getFirst("token");        // 2、校验
        if ("myToken".equals(token)) {            // 3、放行
          	return chan.filter(exchange);
        }        // 4、拦截:禁止访问返回状态码
      	exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);        return exchange.getResponse().setComplete();
    }
}

四、整合Sentinel

Sentinel从1.6.0版本开始提供了Spring Cloud Gateway的适配模块,可以提供两种资源维度的限流

  • route维度:即在Spring 配置文件中配置路由的条目,资源名为对应的routeId自定义

  • API维度:用户可以利用Sentinel提供的API自定义一些API分组

可以直接在网关做流控,具体Sentinel整合跟使用Sentinel差不多,可参考SpringCloud实践:Sentinel流控组件,只有由于Gateway编程模型是reactor-netty导致的,在Gateway中对Sentinel异常的全局捕获方式不同。

4.1、整合Sentinel

  • 引入依赖

    <!-- Sentinel依赖 --><dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId></dependency><!-- 用于Gateway中对Sentinel的异常捕获 --><dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId></dependency>
  • 编写配置文件

    server:
    	port: 8040 # gateway的端口号
    	spring:
    	application: 
    		name: cloud-gateway # 服务名
    	cloud: 
    		# nacos配置
    		nacos:	
    			discovery:
    				server-addr: 127.0.0.1:8848
    				username: nacos        password: nacos
    		gateway:
    			# gateway配置      routes:         - id: cloud-goods  	
    # cloud-goods服务          uri: lb://cloud-goods/
    # cloud-goods服务转发到cloud-goods服务(也可以http://localhost:8081)          predicates:            - Path=/goods
    /** # 只有/goods/**的接口可被访问     sentinel:      transport:        port: 8719 
    # 默认8719(当前服务,对sentinel提供的,调用端口号,用于监控服务访问数据)        dashboard: localhost:8888 # dashboard地址      eager: true 
    # 启动项目是自动注入到sentinel中			

4.2、全局异常捕获

在SpringCloud实践:Sentinel流控组件中,我们已经做了全局的异常捕获,但是那是基于servlet编程模型的,而Gateway 使用的Webflux中的reactor-netty响应式编程组件,底层使用了Netty通讯框架。所以,那种全局捕获方式不适用。

可以导入下面这个依赖,该依赖中有个SentinelGatewayBlockExceptionHander类,可用于Sentinel的全局异常捕获。

<!-- 用于对sentinel的异常捕获 --><dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId></dependency>

4.2.1、原理

【原理】

SentinelGatewayBlockExceptionHandler类中实现了WebExceptionHandler接口,对Gateway中的异常做了判断,如果不是Sentinel的异常(BlockException),Gateway自行处理,如果是Sentinel的异常就响应出Sentinel的结果(Block by Sentinel: FlowException/…)

  • SentinelGatewayBlockExceptionHandler类核心源码

    // 这个方法是私有的无法继承,只能实现并重写该方法,对异常做自定义处理private Mono<Void> writeResponse(ServerResponse response, ServerWebExchange exchange) {     return response.writeTo(exchange, contextSupplier.get());
    }// 这里对异常做判断,如果是Sentinel异常调用writeResponse方法处理@Overridepublic Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {  // 非Sentinel异常Gateway自行处理
      if (exchange.getResponse().isCommitted()) {    return Mono.error(ex);
      }  if (!BlockException.isBlockException(ex)) {    return Mono.error(ex);
      }  // Sentinel异常调用writeResponse方法处理
      return handleBlockedRequest(exchange, ex)
        .flatMap(response -> writeResponse(response, exchange));
    }

由于writeResponse方法是私有的,我们直接继承SentinelGatewayBlockExceptionHandler类无法继承,所以我们也实现WebExceptionHandler接口,然后copy SentinelGatewayBlockExceptionHandler类中的所有方法,并修改writeResponse方法即可。

4.2.2、实现

  • 依赖

    <!-- 用于对sentinel的异常捕获 --><dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId></dependency>
  • 自定义异常处理

    实现WebExceptionHandler接口,copy SentinelGatewayBlockExceptionHandler类中的所有方法,并修改writeResponse方法

    public class MySentinelGatewayBlockExceptionHandler implements WebExceptionHandler {  // 其他方法略
      
      // 自定义异常处理
      private Mono<Void> writeResponse(ServerResponse response, ServerWebExchange exchange){
    		ServerHttpResponse response = exchange.getResopnse();
        response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        Map res = new HashMap() {{
          put("success": false);
          put("msg": "系统繁忙稍后重试");
        }}
        ObjectMapper objectMapper = new ObjectMapper();
        DataBuffer dataBuffer = null;    try {
          jsonStr = objectMapper.writeValueAsString(res);
          dataBuffer = response.bufferFactory.wrap(jsonStr.getBytes());
        } catch (JsonProcessingException e) {
          e.printStackTrace();
        }    return response.writeWith(Flux.just(dataBuffer));
      }
    }
  • 配置类注入

    编写配置类注入自定义的类让其生效

    @Configurationpublic class GatewayConfiguration {  private final List<ViewResolver>viewResolvers;  private final Servercodecconfigurer servercodecconfigurer; 	
      public Gatewayconfiguration(objectProvider<List<viewResolver>>viewResolversProvider, Servercodecconfigurer servercodecconfigurer){    this.viewResolvers = viewResolversProvider.getIfAvailable(Collections:emptyList);    this.servercodecconfigurer servercodecconfigurer;
      }  
      @Bean
      // 必须优先级最高
      @Order (ordered.HIGHEST_PRECEDENCE)  public MySentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler {    return new MySentinelGatewayBlockExceptionHandler(viewResolvers, servercodecconfigurer);
      } 
    }

五、跨域问题

5.1、简述跨域

  • 什么是跨域?

    当一个浏览器请求url的协议域名端口三者之间的任意一个与当前页面url不同即为跨域

  • 跨域产生原因?

    出于浏览器的同源策略限制。

    同源策略(Same Orgin Policy)是一种约定,它是浏览器核心也最基本的安全功能,它会阻止一个域的js脚本和另外一个域的内容进行交互,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。
    所谓同源(即在同一个域)就是两个页面具有相同的协议(protocol)、主机(host)和端口号(port)。

5.2、Gateway跨域问题及解决

5.2.1、Gateway跨域问题

image.png

上图所示,浏览器在访问网关的时候存在跨域,而网关访问服务是不存在跨域的,因为跨域问题是浏览器与服务之间的问题。因此,我们只需要解决浏览器访问网关的跨域问题。

5.2.1、跨域问题解决

  • 方案一:添加配置

    在Gateway服务中配置,解除跨域限制


    • 方法一:配置文件中配置

      image.png

    • 方法二:在启动类里面Bean注入

    image.png

    • 方法三:编写配置类注入

      @Configurationpublic class CorsConfig { 
          @Bean
          public CorsWebFilter corsFilter(){
              UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
              source.registerCorsConfiguration("/**", buildConfig());        return new CorsWebFilter(source);
          } 
          private CorsConfiguration buildConfig(){
              CorsConfiguration corsConfiguration = new CorsConfiguration();
       
              corsConfiguration.addAllowedOrigin("*");
              corsConfiguration.addAllowedHeader("content-type");
              corsConfiguration.addAllowedMethod("POST,GET,OPTIONS,DELETE,PUT");
              corsConfiguration.setMaxAge(3628800L); 
              return corsConfiguration;
          }
      }
  • 方案二:加一层nginx

    常规场景下,是会在浏览器和Gateway之间加一层nginx,来解决跨域。

    比如:浏览器在https://www.baidu.com域名下,该域名默认是443端口,那么nginx监听该域名的443端口,那么在https://www.baidu.com域名下
    1、访问https://www.baidu.com:443/gateway,通过域名解析会访问到指定服务器上监听443的nginx,这个过程是没有涉及跨域的
    2、在nginx里面配置服务转发,转发到gateway,此过程及后面过程,都是服务与服务间通信了,就不会有跨域问题了。

image.png