SpringCloud Alibaba 入门到精通 - Gateway

一、网关简介

网关的概念从开始有网络时基本就存在了,作用一般是鉴权、白名单、跨域设置、负载均衡、灰度发布、失败重试、路由断言等。简单总结来说就是为了做一些统一的不涉及业务活动的动作。将这些动作和业务隔离开来,解耦了这些无关业务的动作,同时也可以使得业务服务更加清晰、安全。
官方文档https://docs.spring.io/spring-cloud-gateway/docs/4.0.x-SNAPSHOT/reference/html/#gateway-starter

本位demo使用的主要版本信息

1.简单使用Gateway网关


使用很简单,直接引入网关依赖即可,我这里使用的是bootstrap配置文件,所以还需要多引入一个bootstrap的依赖,如果只使用application.yml可以不引入bootstrap的依赖。

 <!-- 网关依赖 -->
 <dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-gateway</artifactId>
 </dependency>

 <!-- 引入bootstrap依赖,不引入这个依赖是无法使用bootstrap配置文件的 -->
 <dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-bootstrap</artifactId>
 </dependency>

然后只需要简单的配置即可开始网关的使用了,这里配置下网关的简单路由到淘宝吧。

server:
  port: 9000
spring:
  cloud:
    gateway:
      routes:
        - id: hello_route
          uri: https://www.taobao.com
          predicates:
            - Path=/**

浏览器输入localhost:9000会被直接路由到淘宝了,这样就可以开始网关的使用了。
SpringCloud Alibaba 入门到精通 - Gateway-LMLPHP

二、断言

基础的网关是由多个路由规则构成的,多个路由规则之间是或的关系,每个路由规则都支持配置断言、网关过滤器等。

  • id:配置路由规则的唯一标识,没有作用,但不可以不设置
  • uri:当请求通过了断言,在经过过滤器以后就会根据uri去转发目标请求了
  • predicates:这里配置断言,多个断言使用杠进行分行设置,gateway提供了12种断言,基本可以满足大部分使用场景了
  • filters: Gatewayfilter 在这里声明,Gateway提供了30多种的过滤器,基本用不完。
    这里着重介绍下网关支持的各种断言,注意
  • 1.不同路由规则之间是或的关系,同一个路由规则下的不同断言是且的关系
  • 2.断言和Gatewayfilter的执行谁寻是:断言–>Gatewayfilter
  • 3.有的断言支持配个多个值,有的不支持配置多个值,可以根据一个规则准确判断:断言中的值本来就是键值对的则不支持声明多个值(一行中声明),比如Cookie、Header、Query 他的值本来就是键值对,则不支持在一行声明多个,他们需要使用逗号分割键和值, 比如Host、Path、Method 等他们的值本来就是字符串,则支持在一行声明多个值,使用逗号分割
  • 4.断言时值是键值对的,可以只断言他们的键而不断言值,这样也是支持的
  • 5.断言失败返回的状态码是404

1.After 断言时间

给定一个时间,请求产生的时间只有在这个时间之后才可以通过。
以下是路由配置:

spring:
  cloud:
    gateway:
      enable: true # 默认值,如果导入了gateway的包却不想用,则需要关闭
      routes:
# 1.时间路由断言:After、Before、Between
        - id: after_route
          uri: https://baidu.com
          predicates:
            - After=2023-11-24T15:00:00+08:00[Asia/Shanghai]
            - Path=/one

2.Before 断言时间

给定一个时间,请求产生的时间只有在这个时间之前才可以通过。
以下是路由配置:

spring:
  cloud:
    gateway:
      enable: true # 默认值,如果导入了gateway的包却不想用,则需要关闭
      routes:
# 1.时间路由断言:After、Before、Between
        - id: before_route
          uri: https://jd.com
          predicates:
            - Before=2023-11-21T15:00:00+08:00[Asia/Shanghai]

3.Between 断言时间

给定两个时间,请求产生的时间只有在这两个时间之间才可以通过,第一个为开始时间,第二个为结束时间,两个时间用逗号隔开。
以下是路由配置:

spring:
  cloud:
    gateway:
      enable: true # 默认值,如果导入了gateway的包却不想用,则需要关闭
      routes:
# 1.时间路由断言:After、Before、Between
        - id: between_route
          uri: https://taobao.com
          predicates:
            - Between=2023-11-21T15:00:00+08:00[Asia/Shanghai],2023-11-24T15:00:00+08:00[Asia/Shanghai]

4.Cookie 断言Cookie

断言请求头中的Cookie信息,Cookie信息通常会携带认证等信息,Cookie信息的设置需要依赖服务端返回的Set-cookie信息进行设置,通常以键值对的形式存在,多个键值对使用逗号隔开。Cookie断言不支持多值的配置,因为他的值本身就是键值对,所以这里不支持多值的声明,逗号前轴是键值对,而不是多值。

spring:
  cloud:
    gateway:
      enable: true # 默认值,如果导入了gateway的包却不想用,则需要关闭
      routes:
# 2.Cookie 路由断言 Cookie 指的就是请求头里的Cookie,等号后面第一个值是Cookie中的键名,逗号之后的是键对应的值也可以是正则表达式
           # 如下的配置要求请求头里有这个token值:Cookie : mycookie=mycookievalue
        - id: cookie_route
          uri: https://taobao.com
          predicates:
            - Cookie=mycookie,mycookievalue

这里的mycookie是Cookie中信息的键,mycookievalue则是Cookie信息中的value,只有完全匹配才会断言通过。

5.Header 断言请求头

断言秦秋头中的信息,请求头中的所有信息都可以断言,不过这里与Cookie类似的是不支持多个值的断言,一次只能断言一个。与其他断言类似的是逗号后支持正则表达式,同样也是支持完全匹配的值。

spring:
  cloud:
    gateway:
      enable: true # 默认值,如果导入了gateway的包却不想用,则需要关闭
      routes:
# 3.请求头 路由断言 写法与Cookie类似,等号后面第一个是值,逗号后面是正则表达式,也可以是完全匹配的值
        # 不过同样的是,不可以在一行写多个请求头信息,必须分开写      
        - id: header_route
          uri: https://jd.com
          predicates:
            - Header=Host,jd.com
            - Header=Referer,https://www.baidu.com

断言请求的Host为jd.com,请求头Host用于标识请求的目标主机信息,Referer用于标识请求的来源信息。这两个两个Header断言的关系是且的关系,也及时只有同时满足时断言才会通过。

6.Host 断言请求头中的Host

这个其实和Header是有些重复的,因为Header时支持断言所有请求头信息的。他的使用支持多值的断言,多值中只要有一个满足,断言即通过。

spring:
  cloud:
    gateway:
      enable: true # 默认值,如果导入了gateway的包却不想用,则需要关闭
      routes:
# 4.请求头Host 路由断言,Host可以使用Header来实现,也可以直接使用Host断言实现,多个值使用逗号隔开
        # 这里的逗号隔开表示多个值,与Cookie和Header不同,Cookie和Header逗号表示分割键值对的
        - id: host_route
          uri: https://taobao.com
          predicates:
            - Host=pdd.com,pdd2.com

如上,当请求信息中Host是pdd.com或者pdd2.com时断言才会通过。

7.Method 断言请求方式

断言请求方式,8种请求方式中基本只会用GET、POST、OPTIONS,其他的基本没有用的,所以这里我们可以针对这三种方式进行断言。

spring:
  cloud:
    gateway:
      enable: true # 默认值,如果导入了gateway的包却不想用,则需要关闭
      routes:
# 5.请求Method 路由断言,注意必须大写,多个方法使用逗号隔开是或的关系
        - id: method_route
          uri: https://wymdm.sunac.com.cn/#/login
          predicates:
            - Method=GET,POST,OPTIONS

8.Path 断言请求路径

Path断言是必用的断言,他是根据请求路径进行断言,通常情况下不同的业务服务都是具有不同的路径前缀,Path正式根据这个业务前缀来路由请求到对饮的服务的。

Paht同样支持多个值的声明,使用逗号隔开即可。满足其一即可断言成功。

spring:
  cloud:
    gateway:
      enable: true # 默认值,如果导入了gateway的包却不想用,则需要关闭
      routes:
# 6.Path 路由断言,Path是路径的意思,可以匹配请求路径,这也是最常用的一个路由断言
        # 可以声明多个路由,使用逗号隔开,路径支持统配符/taobao*/** 也是可以的,这种写法下,支持taobao路径开头的任何信息,比如/taobao2222/111
        - id: path_route
          uri: https://taobao.com
          predicates:
            - Path=/taobao/**,/taobao2/**,/taobao*/**

以上断言支持路径/taobao/11/11,/taobao2/11/11,/taobao11111/2222/333

9.Query 断言请求URL参数

注意这里只可以断言请求RUL中携带的参数,对于form-data、json等数据中的参数是无法断言的。断言基本都是支持只断言请求参数而不断言参数值的。

spring:
  cloud:
    gateway:
      enable: true # 默认值,如果导入了gateway的包却不想用,则需要关闭
      routes:
# 7.请求参数Query 路由断言: 这里的请求参数指的是请求头中携带的参数,支持GET、POST
        # 如果是form-data、raw等格式的数据,这里是无法断言的
        # 这里可以只断言参数,也可以既断言参数也断言参数的值,同样支持正则
        - id: query_route
          uri: https://jd.com
          predicates:
            - Query=username
            - Query=password,123456

10.RemoteAddr 断言客户端地址

当没有代理服务器时或者中间服务器时,这个IP是远端IP,如果有代理比如用户访问时经过了NG,那么这个地址其实是NG的地址,而不是远端客户端的真实地址,当不存在代理服务器时可以使用RemoteAddr进行获取客户端地址。

spring:
  cloud:
    gateway:
      enable: true # 默认值,如果导入了gateway的包却不想用,则需要关闭
      routes:
        - id: remote_addr_route
          uri: https://taobao.com
          predicates:
            - RemoteAddr=172.17.64.0/20
            - RemoteAddr=172.17.65.124,172.17.65.123

如上,使用了两种方式对ip进行了断言,
第一种(172.17.64.0/20)较多CIDR表示法,172.17.64.0/20表示ip为172.17.64.0的远端地址是可以访问,此外还表示这个网段下的ip都可以访问,不过是这个ip之后的数据也就是172.17.64.0开始一直到这个网段结束。ip是由网络号和主机号组成的,20表示的是网络号的长度,余下的是主机号。如下:第三位主机号最大还可以加15,第四位则是255,所以允许的ip范围是172.17.64.0-172.17.79.255.

 172     .  17      .  64       . 0
 10101100   00010001   01000000   00000000
 其实知道ip和 网络号,也可以推断出他的子网掩码是(网络号全为1就是子网掩码):
 11111111.11111111.11110000.00000000(子网掩码,二进制表示法)
 255.255.240.0(子网掩码,点十表示法)

第二种则是对两个IP进行了断言。

11.Weight 设置访问权重,并不会断言

这是是针对请求进行进行权重控制,并不会对请求信息进行断言,不知道gateway为什么把他放在了这里,感觉放在过滤器里实现更加合理一些。不过真实场景基本没有人使用gateway提供的权重断言,都是使用负载均衡的中间件来进行负载管控,而不是权重断言。
这里的权重断言使用时一定需要注意的是,,才可以实现权重的路由。

spring:
  cloud:
    gateway:
      enable: true # 默认值,如果导入了gateway的包却不想用,则需要关闭
      routes:
# 9.权重Weight 路由断言,权重路由是权重路由,可以给路由断言设置权重,权重越高,被匹配的概率越大
        # 这里声明多个路由在一个分组的话,只需要将分组名称命名为一样的即可,如果分组名不一样则
        # 他们之间的权重则没有关系,只有同组的权重才会相互影响。
        # 权重一般会配合别的断言一起使用,很少有单独使用的
        - id: weight_route
          uri: https://taobao.com
          predicates:
            - Weight=group1,8
        - id: weight_route2
          uri: https://jd.com
          predicates:
            - Weight=group1,8      

如上多个路由使用了同一个组名group1,他们的权重值是相同的,这样基本就是一个轮询的效果。

12.XForwarded 断言代理地址

RemoteAddr 断言在使用了代理服务器时就无法获取到远端真实的IP了,那怎么才可以断言到真实的客户端的IP呢,就需要使用XForwardedRemoteAddr断言。

spring:
  cloud:
    gateway:
      enable: true # 默认值,如果导入了gateway的包却不想用,则需要关闭
      routes:
        - id: x_forwarded_remote_addr_route
          uri: https://taobao.com
          predicates:
            - XForwardedRemoteAddr=172.17.64.0/20

三、GatewayFilter

网关中的过滤器主要是GatewayFilter和GlobalFilter。GatewayFilter必须配合路由使用,他针对路由中的请求信息进行生效,Gateway中支持的GatewayFilter有30+种,这里挑出了比较重要和真正会用到的过滤器进行总结说明。

1.AddRequestHeader 增加请求头

顾名思义就是可以为请求头添加信息,感觉一般也用不到,想要对信息进行统一处理还是GlobalFilter更好用一些,加入为请求头添加Referer信息。

spring:
  cloud:
    gateway:
      enable: true # 默认值,如果导入了gateway的包却不想用,则需要关闭
      routes:
# 1.增加请求头的网关过滤器 AddRequestHeader
        - id: add_request_header_route
          uri: http://localhost:8001
          predicates:
            - Path=/order/addOrder
#            - Query=username
          filters:
            - AddRequestHeader=Referer,https://jd.com

注意使用filters配置项进行配置,如上配置表示断言满足的情况下给我请求信息添加Referer。

2.AddResponseHeader 增加响应头

这里是为响应头添加信息

spring:
  cloud:
    gateway:
      enable: true # 默认值,如果导入了gateway的包却不想用,则需要关闭
      routes:
# 2.添加响应头的网关过滤器 AddResponseHeader: 需要注意的是如果请求时是从这个路由走的,那么响应时也可肯定是从这个路由走的
        # 如果想要实现给一个请求头塞入多个值并用逗号隔开,最好使用复杂配置项,直接声明name,value的值,如下,当使用简单配置方式时
        # 多次尝试,是无法正常给一个信息赋多个值的
        - id: add_response_header_route
          uri: http://localhost:8001
          predicates:
            - Path=/order/**
          filters:
            - AddResponseHeader=Access-Control-Allow-Methods,POST

注意若是想要给一个请求头信息添加多个值,最好使用复杂方式配置项,而不要使用这种简略的,笔者尝试了多次简略的配置方式配置多个值均不生效,复杂方式需要我们使用name声明过滤器,使用name和value分别声明键和值,如下:

spring:
  cloud:
    gateway:
      enable: true # 默认值,如果导入了gateway的包却不想用,则需要关闭
      routes:
# 2.添加响应头的网关过滤器 AddResponseHeader: 需要注意的是如果请求时是从这个路由走的,那么响应时也可肯定是从这个路由走的
        # 如果想要实现给一个请求头塞入多个值并用逗号隔开,最好使用复杂配置项,直接声明name,value的值,如下,当使用简单配置方式时
        # 多次尝试,是无法正常给一个信息赋多个值的
        - id: add_response_header_route
          uri: http://localhost:8001
          predicates:
            - Path=/order/**
          filters:
            - name: AddResponseHeader
              args:
                name: Access-Control-Allow-Methods
                value: POST,GET

3.StripPrefix 去除请求的前缀路径

通常有时候会让请求信息中携带前缀路径,比如/v1/customer/**、/api/cusomer/*等等。而前缀api、v1又不需要放入到真实的路径中,就可以使用StripPrefix将前缀路径去掉。

spring:
  cloud:
    gateway:
      enable: true # 默认值,如果导入了gateway的包却不想用,则需要关闭
      routes:
# 3.去除路径中的公共部分,余下部分进行路由: StripPrefix
        - id: strip_prefix_route
          uri: http://localhost:8001
          predicates:
            - Path=/api/order/addOrder
          filters:
          # 表示去除第一段路径,即api
            - StripPrefix=1

StripPrefix=1表示去除路径中第一部分,如果是=2则表示去除路径中前两部分。

4.PrefixPath 为请求增加公共前缀

这个与上面正好相反,有公共前缀的又不喜欢前端知道或者想要统一处理,就可以使用这个过滤器。

spring:
  cloud:
    gateway:
      enable: true # 默认值,如果导入了gateway的包却不想用,则需要关闭
      routes:
        - id: prefix_path_route
          uri: http://localhost:8001
          predicates:
            - Path=/api/addOrder
          filters:
            - StripPrefix=1
            - PrefixPath=/order

如上配置表示先去除api再添加order

5.RewritePath 重写请求路径

StripPrefix和PrefixPath都是只可以操作路径的前缀,对于路径中的其他操作就会比较局限,而Gateway提供了更为强大的路径处理的过滤器RewritePath。他可以实现路径的任意组合。

spring:
  cloud:
    gateway:
      enable: true # 默认值,如果导入了gateway的包却不想用,则需要关闭
      routes:
# 5.重写请求路径 RewritePath 下面的写法效果和StripPrefix一样,只不过StripPrefix是去除,而RewritePath是重写
        # 重写路径的写法: RewritePath=/api/?(?<segment>.*), /$\{segment}
        # (?<segment>.*) 表示匹配的部分将其存入到segment
        # /$\{segment}  表示重写的路径: / + 前面匹配的内容
        - id: rewrite_path_route
          uri: http://localhost:8001
          predicates:
            - Path=/api/order/addOrder
          filters:
#            - StripPrefix=1
            - RewritePath=/api/?(?<segment>.*), /$\{segment}

如上配置是将api后的路径取出放置到segment中,然后使用,相当于StripPrefix=1.

6.Retry 失败重试

这是控制请求失败重试的过滤器,当请求经过当前过滤器且失败时可以触发(有条件),根据配置进行间隔性重新请求。主要配置项,有部分有坑。详见注释:

spring:
  cloud:
    gateway:
      enable: true # 默认值,如果导入了gateway的包却不想用,则需要关闭
      routes:
          - id: retry_route
          uri: http://localhost:8001
          predicates:
            - Path=/api/**
          filters:
            - AddRequestHeader=X-Request-Foo, Bar
            - RewritePath=/api/?(?<segment>.*), /$\{segment}
            - name: Retry
              args:
                retries: 3 # 这是默认值,不设置默认就是三次
#                series: SERVER_ERROR # HttpStatus.Series 默认5开头的都会触发
                series: '' # 这个值默认是 SERVER_ERROR ,表示只要是5开头的异常码都会触发重试,若是想要通过statuses 指定
                           # 状态码触发重试,则需要将这个值置空,注意这个是必须的,不可为null
                statuses: INTERNAL_SERVER_ERROR # HttpStatus
                methods: GET,POST # 只针对指定的请求方式进行重试,其他不进行重试
                backoff:
                  firstBackoff: 1000 # 第一次重试间隔时间1s,单位应该是ms
                  maxBackoff: 10000 # 最大间隔时间不能超过10s
                  factor: 2 # 每次重试时等待时间的增长因子。这里是 2,表示每次等待时间都是上一次的两倍。
                  basedOnPreviousValue: true # 是否根据前一次的等待时间来计算下一次的等待时间。false的话则都是根据第一次的也就是firstBackoff来计算

失败重试的基本都是这些配置没啥大区别。

7.Default Filters 默认过滤器

前面说过GatewayFilter只作用于路由规则内部,若是每个路由规则都需要配置相同的过滤器就会很麻烦,默认过滤器就是用来简化这个操作的。默认过滤器内部支持的过滤器就是GatewayFilter的过滤器,配置在默认过滤器的过滤器作用于所有路由规则。注意默认过滤器的执行顺序依然是断言大于过滤器。

server:
  port: 9000
spring:
  cloud:
    gateway:
      default-filters:
        - RewritePath=/api/?(?<segment>.*), /$\{segment}
        - name: Retry
          args:
            retries: 3 # 这是默认值,不设置默认就是三次
            # series: SERVER_ERROR # HttpStatus.Series 默认5开头的都会触发
            series: '' # 这个值默认是 SERVER_ERROR ,表示只要是5开头的异常码都会触发重试,若是想要通过statuses 指定
            # 状态码触发重试,则需要将这个值置空,注意这个是必须的,不可为null
            statuses: INTERNAL_SERVER_ERROR,BAD_GATEWAY,SERVICE_UNAVAILABLE # HttpStatus
            methods: GET,POST # 只针对指定的请求方式进行重试,其他不进行重试
            backoff:
              firstBackoff: 1000 # 第一次重试间隔时间1s,单位应该是ms
              maxBackoff: 10000 # 最大间隔时间不能超过10s
              factor: 2 # 每次重试时等待时间的增长因子。这里是 2,表示每次等待时间都是上一次的两倍。
              basedOnPreviousValue: true # 是否根据前一次的等待时间来计算下一次的等待时间。false的话则都是根据第一次的也就是firstBackoff来计算
      routes:
        - id: task_route
          uri: http://localhost:8001
          predicates:
            - Path=/api/task/**
        - id: data_route
          uri: http://localhost:8002
          predicates:
            - Path=/api/data/**

如上配置默认过滤器会对所有都生效,当接口发生500或者503时都会触发3次重推。

四、GlobalFilter

网关的主要功能除了路由、重试还有鉴权、白名单、日志等,这些则需要依赖全局过滤器。全局过滤器。
注意全局过滤器的执行顺序:断言>网关过滤器>全局过滤器

1.简单使用全局过滤器

只需要实现GlobalFilter、Ordered两个接口即可,GlobalFilter负责处理请求和响应,Ordered负责处理过滤器的优先级问题,如果不实现Ordered接口,使用Order注解效果也是一样的。如下:

package com.cheng.ebbing.gateway.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * @author pcc
 */
@Slf4j
@Order(0)
@Component
public class LogGlobalFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        exchange.getRequest().getQueryParams().forEach((key, value) -> {
            log.info("收到请求参数:{},{}",key,value);
        });
        exchange.getRequest().getHeaders().forEach((key, value) ->{
            log.info("收到请求头参数:{},{}",key,value);
        });
        return chain.filter(exchange);
    }
}

2.全局过滤器如何实现pre、post方法

从上面的代码可以看到,这里的过滤器和WebFilter是类似的都是类似的(Gateway中不可以些WebFilter注解)。

@WebFilter(urlPatterns = "/test")
public class TestWebFilter implements WebFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        return chain.filter(exchange);
    }
}

很明显默认处理的是请求方法,也就是相当于servlet的pre方法,那post方法怎么声明呢?这个需要借助Mono来处理,Mono提供了一个fromRunnable返回一个Mono对象,如下:

package com.cheng.ebbing.gateway.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * @author pcc
 */
@Slf4j
@Order(0)
@Component
public class LogGlobalFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        final Long startTime= System.currentTimeMillis();
        exchange.getRequest().getQueryParams().forEach((key, value) -> {
            log.info("收到请求参数:{},{}",key,value);
        });
        exchange.getRequest().getHeaders().forEach((key, value) ->{
            log.info("收到请求头参数:{},{}",key,value);
        });

        return chain.filter(exchange).then(Mono.fromRunnable(() -> {
            final Long endTime= System.currentTimeMillis();
            // 获取响应状态码
            int statusCode = exchange.getResponse().getStatusCode().value();
            if (statusCode == 200) {
                log.info("响应信息正确");
            } else {
                log.info("响应信息错误,错误码:{},处理用时:{}",statusCode,(endTime-startTime));

            }
        }));
    }
}

3.如何实现异常时返回json给到客户端

网关既然起到了鉴权、白名单等拦截的作用,那异常场景是需要直接返回给客户端的,Json格式的数据是目前最普遍的交互方式。针对上面的代码我们可以做如下改动:

package com.cheng.ebbing.gateway.filter;

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
 * @author pcc
 */
@Slf4j
@Order(0)
@Component
public class LogGlobalFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        final Long startTime= System.currentTimeMillis();
        exchange.getRequest().getQueryParams().forEach((key, value) -> {
            log.info("收到请求参数:{},{}",key,value);
        });
        exchange.getRequest().getHeaders().forEach((key, value) ->{
            log.info("收到请求头参数:{},{}",key,value);
        });
        
        // 假设异常触发了
        if(1>1){
            //组装异常信息
            Map<Object, Object> objectObjectMap = new HashMap<>();
            objectObjectMap.put("code", HttpStatus.BAD_REQUEST.value());
            objectObjectMap.put("message","请求处理异常");
            byte[] bytes = JSON.toJSONString(objectObjectMap).getBytes(StandardCharsets.UTF_8);
            DataBuffer read = exchange.getResponse().bufferFactory().wrap(bytes);
            exchange.getResponse().getHeaders().add("Content-Type", "application/json;charset=UTF-8");
            return exchange.getResponse().writeWith(Mono.just(read));
        }


        return chain.filter(exchange).then(Mono.fromRunnable(() -> {
            final Long endTime= System.currentTimeMillis();
            // 获取响应状态码
            int statusCode = exchange.getResponse().getStatusCode().value();
            if (statusCode == 200) {
                log.info("响应信息正确");
            } else {
                log.info("响应信息错误,错误码:{},处理用时:{}",statusCode,(endTime-startTime));
            }
        }));
    }
}

五、网关的CORS配置与超时配置

网关作为系统门户自然需要处理CORS的问题。在不使用网关时我们可以使用注解@CrossOrigin或者利用WebFilter是可以解决这个问题的,在网关里也提供了一套自己的解决方案,不过这些方案都是殊途同归,他们都是通过设置以下请求头属性来声明是否允许跨域的。

  • Origin:允许访问的源
  • Method:允许访问的请求方式
  • Header:允许的请求头
  • Credentials:允许携带认证信息进行访问,即请求头中的Authorization、Cookie等认证信息

1.全局CORS配置-配置类

如下,提供一个Cors的配置类,这些信息都差不多,然后将他交给Gateway的请求拦截即可。
,因为当浏览器跨域时默认发送OPTIONS请求去服务器端检测是否允许跨域,是否允许就是根据响应头中的AllowedOrigin来判定的。

package com.cheng.ebbing.gateway.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;

import java.util.Arrays;

/**
 * @author pcc
 */
@Configuration
public class GatewayConfig {

    @Bean
    public CorsConfiguration getCorsConfiguration() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowedOriginPatterns(Arrays.asList("http://localhost:*",""));
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.setAllowedMethods(Arrays.asList("GET","POST","OPTIONS"));
        corsConfiguration.setAllowCredentials(true);
        corsConfiguration.setMaxAge(3600L);// 预检请求的有效期,有效期内浏览器可以不用重复预检
        return corsConfiguration;
    }

    @Bean
    public CorsWebFilter corsWebFilter(CorsConfiguration corsConfiguration) {
        UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
        urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
        return new CorsWebFilter(urlBasedCorsConfigurationSource);
    }
}

2.全局CORS配置-配置文件

配置文件的配置位置如下,信息还都是上面那些信息

spring:
  cloud:
    gateway:
      globalcors:
      globalcors:
        cors-configurations:
          '[/**]':
            allowedOrigins: "http://localhost:5173"
            allowedHeaders: "*"
            allowedMethods:
              - OPTIONS  # 注意 跨域设置OPTIONS是必须的,因为前端跨域时浏览器默认发出OPTIONS请求
              - GET
              - POST
            allowCredentials: true
            maxAge: 3000  

3.局部CORS配置作用于路由

Gateway官网说是支持路由中配置跨域的,不过他们给出的配置是错的,目前没有修正,这个场景使用也比较少,可以不用在意,以下是官网给的配置:

spring:
  cloud:
    gateway:
      routes:
      - id: cors_route
        uri: https://example.org
        predicates:
        - Path=/service/**
        metadata:
          cors:
            allowedOrigins: '*'
            allowedMethods:
              - GET
              - POST
            allowedHeaders: '*'
            maxAge: 30

4.超时配置全局

这里httpclient并不是说gateway使用的是httpclient进行请求转发的,事实上底层他使用的是netty进行通讯的,不过这个是可以更改的,可以更改为常用的okhttp等都是支持的,当然修改的话需要提供配置。
注意connect-timeout单位是ms,下面是携带单位的5s

spring:
  cloud:
    gateway:
      httpclient:
        connect-timeout: 1000
        response-timeout: 5s

5.超时配置局部

超时局部配置也是通过元信息的配置设置的,这个是有用的,通常个别接口我们是需要设置较长的响应时间的
注意这里单位都是ms

spring:
  cloud:
    gateway:
      routes:
        - id: per_route_timeouts
          uri: https://example.org
          predicates:
            - Path: /**
          metadata:
            response-timeout: 200
            connect-timeout: 200

六、网关整合Nacos+Loadbalancer实现负载均衡

在SpringCloud中服务都是注册到注册中心的,所以路由时我们直接访问ip是不合适的,所以这里我们需要引入注册中心nacos,使用nacos和OpenFeign进行调用时,负载均衡的中间件是必须引入,否则无法实现正常调用,因为SringCloud默认使用,这里也是一样使用nacos+gateway进行调用,但负载均衡还是得LoadBalancer来。

1.引入依赖

        <!-- nacos注册中心客户端,父工程引入了cloud的包管理,这里无需声明具体包-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <!-- 引入loadbalancer负载均衡,父工程引入了cloud的包管理,这里无需声明具体包-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>

2.配置更改

负载均衡组件是个比较简单的组件,如果不更换负载策略可以不增加任何配置,我们只需要配置nacos的信息即可。Nacos使用也很简单,无需增加额外注解(Cloud帮我们省略了)只需要配置下配置信息即可,如对nacos和LoadBalancer有疑问可以看这里:https://blog.csdn.net/m0_46897923/article/details/132639913

server:
  port: 9000
spring:
  application:
    name: ebbing-gateway
  cloud:
    nacos:
      server-addr: 192.168.150.197:8848
      user-name: nacos # 这是默认值
      password: nacos # 这是默认值
      discovery:
        namespace: 78ec0572-2091-43de-a708-5978caee7b3a # dev
        watch:
          enable: true
        heart-beat-interval: 5000

不错只需要这些更改就可以集成了,事实上gateway也是标准的boot项目,所以没有任何多余的操作就可以直接引入。

3.实现负载均衡

nacos与loadbalance引入就是这么简单,使用的话我们只需要在uri中更改路由的路径即可,未更改前是:http://localhost:8808,更改后如下:

spring:
  application:
    name: ebbing-gateway
  cloud:
    gateway:
      routes:
        - id: task_route
          uri: lb://ebbing-task
          predicates:
            - Path=/api/task/**

lb是使用负载组件,ebbing-task是注册中心的服务名称。LoadBalancer是客户端负载,原理是从nacos拉取所有实例信息,在本地实现的负载。

4.自动获取nacos服务名实现路由

如果网关中无需为每个项目配置单独的断言或者路由规则,那么我们可以使用gateway提供的自动映射,配置如下所示。

spring:
  application:
    name: ebbing-gateway
  cloud:
    nacos:
      server-addr: 192.168.150.197:8848
      user-name: nacos # 这是默认值
      password: nacos # 这是默认值
      discovery:
        locator:
          enabled: true # 开启主动映射,自动映射后可以实现注册中心的服务自动路由,原理是路径必须以nacos服务名开头
          lower-case-service-id: true # 开启小写支持,注册中心服务名默认大写,开启后支持小写
        namespace: 78ec0572-2091-43de-a708-5978caee7b3a # dev
        watch:
          enable: true
        heart-beat-interval: 5000

不过一般不用,大部分长还是需要单独配置路由的,不过即使配置了自动路由,也是支持手写路由的,且手写路由的优先级会更高一些。

七、网关整合Sentinel进行流控

这里不详细介绍Sentinel的配置和使用,Sentinel可以参见这里哦:https://java-dream.blog.csdn.net/article/details/132796974

1.引入依赖

这里除了需要引入sentinel的依赖外,还需要引入一个整合的依赖,不过都不需要我们声明版本号,版本号直接在父工程定义即可,具体版本信息详见文章开头。

        <!-- 网关和sentinel整合包 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
        </dependency>
        <!--引入sentinel -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>

2.配置更改

如果想要使用sentinel的dashboard则需要添加配置,如果不需要使用dashboard而是直接使用api进行流控,则可以任何配置都不添加。以下配置是sentinel的dashboard的配置,与整合没有关系其实。

spring:
  application:
    name: ebbing-gateway
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8088
        port: 8719 # gateway与dashboard通信的本地端口

3.dashboard配置路由流控

Gateway整合Sentinel不支持Sentinel中的授权规则,其他的熔断、流控、热点流控、系统规则基本算是支持,使用与Sentinel的原本配置几乎没有差别。这里要说的是使用dashboard配置路由流控,sentinel支持对gateway的两种流控方式,一种是路由流控,一种是根据请求的接口进行流控,也就是api流控了。
SpringCloud Alibaba 入门到精通 - Gateway-LMLPHP
上图是接口被触发了以后就可以看到路由规则对应的请求链路信息了,点击流控进行配置流控规则:
SpringCloud Alibaba 入门到精通 - Gateway-LMLPHP
添加完规则以后,通过jemter测试,如下:
SpringCloud Alibaba 入门到精通 - Gateway-LMLPHP
可以发现被流控了,不过这里都是抛出的ParamFlowException.

4.dashboard配置api流控

这里的api流控,就是可以根据我们自己添加接口进行流控管理,如下图:

SpringCloud Alibaba 入门到精通 - Gateway-LMLPHP
SpringCloud Alibaba 入门到精通 - Gateway-LMLPHP
然后还回到请求链路中随意找一个路由点击添加流控,然后操作如下,即可选择到刚刚添加的API了,其他操作和路由流控就没有区别了。
SpringCloud Alibaba 入门到精通 - Gateway-LMLPHP

5.api配置路由流控

上面已经说了dashboard配置流控的方式,那如何使用api进行配置流控呢,这里使用路由规则进行演示API配置流控,代码如下:

package com.cheng.ebbing.auth.config;

import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;

import javax.annotation.PostConstruct;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

/**
 * @author pcc
 */
@Configuration
public class GatewayConfig {

    @PostConstruct
    public void init() {
        // 初始化流控规则
        GatewayFlowRule flowRule = new GatewayFlowRule("task_route");// 路由名
        flowRule.setCount(2);// 并发2
        flowRule.setIntervalSec(1);//统计时间1s
        flowRule.setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_ROUTE_ID);// 默认使用路由模式
        flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS);// 默认使用QPS

        Set<GatewayFlowRule> set = new HashSet<GatewayFlowRule>();
        set.add(flowRule);
        GatewayRuleManager.loadRules(set);
    }

}

代码如上,验证都没有区别,这里不重复贴图了,可以看到代码和sentinel本身使用时非常相似,只是类名上都多了个gateway而已。

6.自定义流控、熔断响应信息

流控一直返回系统定义的异常信息肯定是不合适的,我们需要定义自己的响应异常信息,如下:

package com.cheng.ebbing.auth.config;

import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;

import javax.annotation.PostConstruct;
import java.util.*;

/**
 * @author pcc
 */
@Configuration
public class GatewayConfig {

    // 定义流控等异常信息
    @PostConstruct
    public void initBlockHanler(){
        GatewayCallbackManager.setBlockHandler((exchange,throwable)->{
            if(throwable instanceof ParamFlowException){

                //组装返回信息
                Map<String,Object> resultMap = new HashMap<String,Object>();
                resultMap.put("code",HttpStatus.TOO_MANY_REQUESTS.value());
                resultMap.put("message",HttpStatus.TOO_MANY_REQUESTS.getReasonPhrase());
                resultMap.put("data",null);

                // 流控异常
                return ServerResponse
                        .status(HttpStatus.TOO_MANY_REQUESTS)
                        .contentType(MediaType.APPLICATION_JSON_UTF8).body(BodyInserters.fromValue(resultMap));

            }else{
                //组装返回信息
                Map<String,Object> resultMap = new HashMap<String,Object>();
                resultMap.put("code",HttpStatus.SERVICE_UNAVAILABLE);
                resultMap.put("message","其他异常");
                resultMap.put("data",null);

                // 其他异常
                return ServerResponse
                        .status(HttpStatus.SERVICE_UNAVAILABLE)
                        .contentType(MediaType.APPLICATION_JSON_UTF8).body(BodyInserters.fromValue(resultMap));
            }
        });
    }
}

可以看到,响应信息是我们自己定义的异常信息了,这样就实现了自定义异常信息。
SpringCloud Alibaba 入门到精通 - Gateway-LMLPHP

7.网关的全局异常处理

会有一些情况网关这里依然获取到了注册中心的实例,但是调用时失败了(并没有到达目标服务),那么此时网关就需要一个类似全局异常处理的机制。怎么做呢,只需要实现ErrorWebExceptionHandler这个函数式接口即可,代码如下:

package com.cheng.ebbing.auth.config;

import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;

import javax.annotation.PostConstruct;
import java.util.*;

/**
 * @author pcc
 */
@Configuration
public class GatewayConfig {

    // 全局异常处理
    @Bean
    @Order(-100)
    public ErrorWebExceptionHandler getGlobalErrorHandler(ObjectMapper objectMapper) {
        return (exchange,throwable)->{
            Map<String,Object> resultMap = new HashMap<String,Object>();
            // 获取返回
            ServerHttpResponse response = exchange.getResponse();
            // 响应类型统一设置为json
            response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
            // 信息已经响应了,异步的存在已经响应过的可能
            if(response.isCommitted()){
                return Mono.error(throwable);
            }

            // 这里可以根据异常类型进行处理了
            if(throwable instanceof RuntimeException){
                response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
                return response.writeWith(Mono.fromSupplier(()->{
                    // 这里手动生成一个mono用以组装返回信息
                    DataBufferFactory bufferFactory = response.bufferFactory();
                    resultMap.put("code",HttpStatus.INTERNAL_SERVER_ERROR.value());
                    resultMap.put("message","路由异常了");
                    try {
                        return bufferFactory.wrap(objectMapper.writeValueAsBytes(resultMap));
                    }
                    catch (JsonProcessingException e) {
                        return bufferFactory.wrap(new byte[0]);
                    }
                }));
            }else{
                // 这里简单处理,真实场景可以根据不同异常进行设置
                response.setStatusCode(HttpStatus.BAD_REQUEST);
                return response.writeWith(Mono.fromSupplier(()->{
                    // 这里手动生成一个mono用以组装返回信息
                    DataBufferFactory bufferFactory = response.bufferFactory();
                    resultMap.put("code",HttpStatus.INTERNAL_SERVER_ERROR.value());
                    resultMap.put("message","其他错误");
                    try {
                        return bufferFactory.wrap(objectMapper.writeValueAsBytes(resultMap));
                    }
                    catch (JsonProcessingException e) {
                        return bufferFactory.wrap(new byte[0]);
                    }
                }));
            }

        };
    }
}

总结

gateway东西其实不多,不过因为其是响应式编程风格,很多东西和Springmvc差别还是很大的。这里总结了断言、过滤器、cors、以及整合nacos、sentinel等信息,其实没有什么困难信息,都是api,使用时翻起来看看即可,希望可以帮助到路过的朋友。

12-15 11:04