Bootstrap

详解网关Gateway

目录

前言

1. Gateway网关作用介绍

1.1 为什么需要网关

2. Gateway网关快速入门

3. 断言工厂 

 4. 过滤器工厂

4.1 路由过滤器的种类

4.2 请求头过滤器

4.3 默认过滤器

4.4 总结

5. 全局过滤器

5.1 全局过滤器的作用

5.2 自定义全局过滤器

5.3 过滤器执行顺序

6. 跨域问题

6.1 什么是跨域问题

6.2 模拟跨域问题

6.3 解决跨域问题


前言

大家都知道,从一个房间走到另一个房间,必然要经过一扇门。同样,从一个网络向另一个网络发送消息,也必须经过一道“关口”,这道关口就是网关。网关又称网间连接器、协议转换器。现实中最常见的就是VPN,它就是一个出口网关。

1. Gateway网关作用介绍

SpringCloudGateway的中文网址:Spring Cloud Gateway 中文文档

Spring Cloud Gateway提供了一个建立在Spring生态系统之上的API网关,包括:Spring 6、Spring Boot 3和project Reactor。Spring Cloud Gateway旨在提供一种简单而有效的方式来路由到api,并为它们提供横切关注点,例如:安全性、监控/指标和弹性。

1.1 为什么需要网关

试想一下,我们现在开发了一个微服务项目,但是这个项目不对外开放,只对内部员工开放,这时候我们是不是需要一个门卫来校验来访者的身份?Gateway网关是我们服务的门卫大叔,所有微服务的统一入口。如下图:
 

网关主要有3个核心功能特性:

  • 权限控制:网关作为微服务入口,需要校验用户是否有请求资格,如果没有则进行拦截。
  • 路由和负载均衡(请求路由):一切请求都必须先通过gateway,但网关不处理业务,而是根据某种规则,把请求转发到某个微服务,这个过程叫做路由。当然路由的目标服务有多个时,还需要做负载均衡。
  • 请求限流:当请求流量过高时,在网关中按照下游的微服务能够接受的速度来放行请求,避免服务压力过大。

在SpringCloud中网关的实现包括两种:gatewayzuul

Zuul是基于servlet的实现,属于阻塞式编程。而SpringCloudGateway则是基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能

2. Gateway网关快速入门

 下面,我就以自己的cloud-demo工程来演示网关的基本路由功能。基本步骤如下:

  1. 创建SpringBoot工程gateway,引入网关和nacos依赖
  2. 编写启动类
  3. 编写基础配置和路由规则
  4. 启动网关服务进行测试

1)创建gateway服务,引入依赖:

创建gateway服务:

 

在gateway服务的pom文件中添加依赖:

<dependencies>
    <!--nacos服务发现依赖-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!--gateway网关依赖-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
</dependencies>

2)编写启动类

 创建启动类GatewayAppliction,内容如下:

package cn.itcast.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}

 3)编写基础配置和路由规则 

创建application.yml文件,内容如下:

server:
  port: 10010
spring:
  application:
    name: gateway #服务名称
  cloud:
    nacos:
      server-addr: localhost:8848 #nacos地址
    gateway:
      routes: #网关路由配置
       - id: user-service #路由id,自定义,只要保持唯一就行
#         uri: http://localhost:8081 #目标服务地址,http指固定地址
         uri: lb://userservice #目标服务地址,lb表示负载均衡,后面跟服务名称
         predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
            - Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
       - id: order-service #路由id,自定义,只要保持唯一就行
         uri: lb://orderservice #目标服务地址,lb表示负载均衡,后面跟服务名称
         predicates:
           - Path=/order/** # 这个是按照路径匹配,只要以/order/开头就符合要求

  •  这里将符合Path规则的一切请求,都代理到uri参数指定的地址。
  • 本例中,将/user/**开头的请求,代理到 lb://userservice,将/order/**开头的请求代理到 lb://orderserivce,lb是负载均衡的缩写,根据服务名称拉取服务列表,实现负载均衡。

 4)启动网关服务进行测试

重启网关,访问http://localhost:10010/user/1时,符合/user/**规则,请求转发到uri:http://userservice/user/1,得到结果:

访问http://localhost:10010/order/101时,符合/order/**规则,请求转发到uri:http://orderservice/order/101,得到结果:

5)网关路由的流程图

整个访问的流程如下:

6)总结

网关搭建步骤:

  1. 创建项目,引入nacos服务发现和gateway依赖
  2. 配置application.yml,包括服务基本信息、nacos地址、路由

路由配置包括:

  1.  路由id:路由的唯一标示
  2. 路由目标(uri):路由的目标地址,http代表固定地址,lb代表根据服务名负载均衡
  3. 路由断言(predicates):判断路由的规则
  4. 路由过滤器(filters):对请求或响应做处理(下面会详细介绍)

3. 断言工厂 

我们在配置文件中写的断言规则只是字符串,这些字符串会被RoutePredicateFactory读取并处理,转变为判断的条件。我们可以看到RoutePredicateFactory这个接口的实现类有十几个:

例如Path=/user/**是啊找路径匹配,这个规则是由org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory类来处理的,像这样的断言工厂在SpringCloudGateway还有十几个,跟RoutePredicateFactory接口的实现类对应:

名称说明示例
After是某个时间点后的请求- After=2037-01-20T17:42:47.789-07:00[America/Denver]
Before是某个时间点之前的请求- Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai]
Between是某两个时间点之前的请求- Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver]
Cookie请求必须包含某些cookie- Cookie=chocolate, ch.p
Header请求必须包含某些header- Header=X-Request-Id, \d+
Host请求必须是访问某个host(域名)- Host=.somehost.org,.anotherhost.org
Method请求方式必须是指定方式- Method=GET,POST
Path请求路径必须符合指定规则- Path=/red/{segment},/blue/**
Query请求参数必须包含指定参数- Query=name, Jack或者- Query=name
RemoteAddr请求者的ip必须是指定范围- RemoteAddr=192.168.1.1/24
Weight权重处理

我们再来试试After这个路由规则:

这里的规则表明orderservice在2031年4月13日的15:14:47之后才会转发。我们重启一下,访问http://localhost:10010/order/101,结果如下:

现在时间是2024年7月23日的18:04:01并不在2031年4月13日的15:14:47之后,所以请求没办法转发,直接报了404。其他规则如果感兴趣的话,可以自己实践一下。

我们只需要掌握Path这种路由规则就可以了。  

 4. 过滤器工厂

GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求微服务返回的响应做处理:

4.1 路由过滤器的种类

Spring提供了30多种不同的路由过滤工厂。如下图:

这里只举了几个过滤器用法的例子,想要知道全部过滤器的用法,可以去官网文档看看:

名称说明
AddRequestHeader给当前请求添加一个请求头
RemoveRequestHeader移除请求中的一个请求头
AddResponseHeader给响应结果中添加一个响应头
RemoveResponseHeader从响应结果中移除有一个响应头
RequestRateLimiter限制请求的流量

4.2 请求头过滤器

下面我们以AddRequestHeader为例来讲解。

需求:给所有进入userservice的请求添加一个请求头:Truth=Hello World!

只需要修改gateway服务的application.yml文件,添加路由过滤即可:

server:
  port: 10010
spring:
  application:
    name: gateway #服务名称
  cloud:
    nacos:
      server-addr: localhost:8848 #nacos地址
    gateway:
      routes: #网关路由配置
       - id: user-service #路由id,自定义,只要保持唯一就行
#         uri: http://localhost:8081 #目标服务地址,http指固定地址
         uri: lb://userservice #目标服务地址,lb表示负载均衡,后面跟服务名称
         predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
            - Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
         filters:
            - AddRequestHeader=Truth,Hello World! # 添加请求头
       - id: order-service #路由id,自定义,只要保持唯一就行
         uri: lb://orderservice #目标服务地址,lb表示负载均衡,后面跟服务名称
         predicates:
           - Path=/order/** # 这个是按照路径匹配,只要以/order/开头就符合要求
#           - After=2031-04-13T15:14:47.433+08:00[Asia/Shanghai]

重启gateway服务和userservice服务,访问http://localhost:10010/user/1,使用Wireshark抓包,发现了添加的请求头信息:

 访问http://localhost:10010/order/101,使用Wireshark抓包,没有发现了添加的请求头信息:

由此可见, 当前过滤器写在userservice路由下,因此仅仅对访问userservice的请求有效

4.3 默认过滤器

如果要对所有的路由都生效,则可以将过滤工厂写道default下。格式如下:

server:
  port: 10010
spring:
  application:
    name: gateway #服务名称
  cloud:
    nacos:
      server-addr: localhost:8848 #nacos地址
    gateway:
      routes: #网关路由配置
       - id: user-service #路由id,自定义,只要保持唯一就行
#         uri: http://localhost:8081 #目标服务地址,http指固定地址
         uri: lb://userservice #目标服务地址,lb表示负载均衡,后面跟服务名称
         predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
            - Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
#         filters:
#            - AddRequestHeader=Truth,Hello World! # 添加请求头
       - id: order-service #路由id,自定义,只要保持唯一就行
         uri: lb://orderservice #目标服务地址,lb表示负载均衡,后面跟服务名称
         predicates:
           - Path=/order/** # 这个是按照路径匹配,只要以/order/开头就符合要求
      default-filters: # 所有路由生效的默认路由过滤器
        - AddRequestHeader=Truth,Hello World! # 添加请求头

这里的default-filters要跟routes对齐,然后重启gateway服务。访问http://localhost:10010/user/1,使用Wireshark抓包,发现了添加的请求头: 

  访问http://localhost:10010/order/101,使用Wireshark抓包,发现了添加的请求头: 

PS:这里抓取的都是转发后的userservice和orderservice实例端口,不能抓取网关10010的端口。

4.4 总结

过滤器的作用是什么?

  1. 对路由的请求或响应做加工处理,比如添加请求头
  2. 配置在路由下的过滤器只对当前路由的请求生效

defaultFilters的作用是什么?

  1. 对所有路由都生效的过滤器 

5. 全局过滤器

在上面过滤器工厂一节介绍,网关提供了30多种过滤器,但每一种过滤器的作用都是固定的。如果我们希望拦截请求,做自己的业务逻辑是没有办法实现的。这时候gateway给我们提供了一个全局过滤器的接口GlobalFilter

5.1 全局过滤器的作用

全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用是一样的。区别就在于GatewayFilter是通过配置定义的,处理逻辑是固定的;而GlobalFilter的逻辑需要自己写代码实现。

定义方式也很简单,就是实现GlobalFilter接口:

 在filter中编写自定义逻辑,可以实现下列功能:

  • 登陆状态校验
  • 权限校验
  • 请求限流等

5.2 自定义全局过滤器

 上面提到自定义全局过滤器只要实现GlobalFilter接口就可以,下面我来提供一个案例。

需求:定义全局过滤器,拦截请求,判断请求的参数是否满足下面条件:

  • 参数中是否有authorization
  • authorization参数是否为admin

如果同时满足就放行,不满足就拦截

在gateway服务中定义一个过滤器:

package cn.itcast.gateway.filters;

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Order(-1)  // 优先级,数字越小,优先级越靠前,aop层面源码很多都用到它
@Component
public class AuthorizeFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //1.获取请求参数
        ServerHttpRequest request = exchange.getRequest();
        MultiValueMap<String, String> params = request.getQueryParams();
        //2.获取authorization参数
        String auth = params.getFirst("authorization");
        //3.判断authorization是否为admin
        if ("admin".equals(auth)) {
            //4.如果为admin,放行
            return chain.filter(exchange);  //执行下一个过滤器
        }
        //5.否则,拦截
        //5.1 设置响应状态码
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); //401,未授权
        //5.2 拦截
        return exchange.getResponse().setComplete();
    }
}

如果不想用@Order注解,也可以实现Ordered接口:

package cn.itcast.gateway.filters;

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

//@Order(-1)  // 优先级,数字越小,优先级越靠前,aop层面源码很多都用到它
@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //1.获取请求参数
        ServerHttpRequest request = exchange.getRequest();
        MultiValueMap<String, String> params = request.getQueryParams();
        //2.获取authorization参数
        String auth = params.getFirst("authorization");
        //3.判断authorization是否为admin
        if ("admin".equals(auth)) {
            //4.如果为admin,放行
            return chain.filter(exchange);  //执行下一个过滤器
        }
        //5.否则,拦截
        //5.1 设置响应状态码
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); //401,未授权
        //5.2 拦截
        return exchange.getResponse().setComplete();
    }

    @Override
    public int getOrder() {
        return -1;
    }
}

我这里用的注解方式,然后重启gateway服务,访问http://localhost:10010/order/101显示的就是401:

加上authorization=admin,访问http://localhost:10010/order/101?authorization=admin ,就能正常访问:

5.3 过滤器执行顺序

 请求进入网关后会碰到三类过滤器:当前路由的过滤器、DefaultFilter、GlobalFilter,如下图:

在前面几个小节,我们知道当前路由的过滤器和DefaultFilter都是通过配置定义的AddRequestHeader,到idea里可以查到AddRequestHeaderGatewayFilterFactory类:

AddRequestHeaderGatewayFilterFactory会去读取配置文件生成一个真正的过滤器(GatewayFilter),所以我们认为当路由过滤器和DefaultFilter是同一类,都叫GatewayFilter。但是全局过滤器叫GlobalFilter,跟这两个好像不一样。别着急,在网关里面有一个东西叫做过滤器适配器(GatewayFilterAdapter):

 GatewayFilterAdapter实现了接口GatewayFilter,这时候我传了一个GlobalFilter,然后GatewayFilterAdapter将GlobalFilter适配成了一个GatewayFilter,也就是说在网关当中的所有的GlobalFilter都可以被适配成GatewayFilter。这样来说,我们是不是可以将网关中的所有过滤器都称为GatewayFilter类型。那么也就是说,请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter,合并到一个过滤器链(List集合)中,排序后依次执行每个过滤器:

那么排序的规则是什么呢?

  • 每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前

  • GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值,由我们自己指定

  • 路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增。

  • 当过滤器的order值一样时,会按照 defaultFilter > 路由过滤器 > GlobalFilter的顺序执行。

详细内容,可以查看源码:

org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#getFilters()方法是先加载defaultFilters,然后再加载某个route的filters,然后合并。

org.springframework.cloud.gateway.handler.FilteringWebHandler#handle()方法会加载全局过滤器,与前面的过滤器合并后根据order排序,组织过滤器链。

6. 跨域问题

6.1 什么是跨域问题

跨域:域名不一致就是跨域,主要包括:

跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题。

解决方案:CORS。不知道的小伙伴可以查看https://www.ruanyifeng.com/blog/2016/04/cors.html

6.2 模拟跨域问题

这里以我自己的cloud-demo写了一个index.html文件。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<pre>
spring:
  cloud:
    gateway:
      globalcors: # 全局的跨域处理
      add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
      corsConfigurations:
        '[/**]':
          allowedOrigins: # 允许哪些网站的跨域请求
            - "http://localhost:8090"
            - "http://127.0.0.1:8090"
          allowedMethods: # 允许的跨域ajax的请求方式
            - "GET"
            - "POST"
            - "DELETE"
            - "PUT"
            - "OPTIONS"
          allowedHeaders: "*" # 允许在请求中携带的头信息
          allowCredentials: true # 是否允许携带cookie
          maxAge: 360000 # 这次跨域检测的有效期
</pre>
</body>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
  axios.get("http://localhost:10010/user/1?authorization=admin")
  .then(resp => console.log(resp.data))
  .catch(err => console.log(err))
</script>
</html>

保存下来。我这里用VSCode启动访问。如果你想试试的话,把这里的地址改成你项目的本地地址, 就可以使用了,放入tomcat或者nginx这样的web服务器中,启动并访问,就可以模拟了:

可以在浏览器控制台看到下面的错误:

从127.0.0.1:8090访问localhost:10010,端口不同,显然是跨域请求。

6.3 解决跨域问题

在gateway服务的application.yml文件中,添加下面的配置:

spring:
  cloud:
    gateway:
      # 。。。
      globalcors: # 全局的跨域处理
        add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
        corsConfigurations:
          '[/**]':
            allowedOrigins: # 允许哪些网站的跨域请求 
              - "http://localhost:8090"
              - "http://127.0.0.1:8090"
            allowedMethods: # 允许的跨域ajax的请求方式
              - "GET"
              - "POST"
              - "DELETE"
              - "PUT"
              - "OPTIONS"
            allowedHeaders: "*" # 允许在请求中携带的头信息
            allowCredentials: true # 是否允许携带cookie
            maxAge: 360000 # 这次跨域检测的有效期

 重启gateway服务,然后刷新界面,就没有报错,也能正常返回参数了:

;