掘金 后端 ( ) • 2024-04-29 14:55

一、网关是什么?

微服务的网关是一个充当服务的入口和出口的服务器,它接收客户端的请求并将其路由到适当的微服务实例。网关还可以处理一些共享的功能,如认证、授权、负载均衡、日志记录和监控等。通过使用网关,可以简化微服务架构中的通信和管理,提高系统的可靠性和安全性。

image-20211114103052970.png

二、什么是Spring Cloud Gateway?

Spring Cloud Gateway是Spring基于Spring5.0、SpringBoot2.0、Project Reactor等技术开发的网关技术

  • 旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。

  • 它不仅提供统一的路由方式,并且基于Filter链的方式提供了网关基本的功能,例如:安全,监控和限流。

  • 它是用于代替NetFlix Zuul的一套解决方案:webflux

Spring Cloud Gateway组件的核心是一系列的过滤器,通过过滤器可以将客户端的请求转发到应用的微服务(这个过程叫路由)。Spring Cloud Gateway是站在整个微服务最前沿的防火墙和代理器,隐藏微服务节点的ip信息、从而加强安全保护。

Spring Cloud Gateway本身也是一个微服务,需要注册到注册中心

Spring Cloud Gatewa的核心功能是:路由和过滤

三、网关入门

1. 核心概念

路由Route

一个路由的配置信息,由一个id、一个目的地url、一组断言工厂、一组过滤器组成。

断言Predicate

断言是一种判断规则;如果客户端的请求符合要求的规则,则这次请求将会被路由送到目的地

Spring Cloud Gateway的断言函数输入类型是Spring5.0框架中的ServerWebExchange,它允许开发人员自定义匹配来自HTTP请求中任何信息

过滤器Filter

Spring Cloud Gateway中的Filter可以对请求和响应进行过滤修改。是一个标准的Spring WebFilter。它分为两类:

  • Gateway Filter:局部过滤器(路由过滤器),应用于单个路由或者一组路由,通常由SpringCloudGateway内置好
  • Global Filter:全局过滤器,应用于所有路由

2. 使用步骤

  1. 创建一个模块:网关模块,导入gateway的依赖
  2. 创建引导类:开启服务注册@EnableDiscoveryClient
  3. 创建配置文件:
    • 网关的端口
    • 设置服务名称
    • 设置注册中心的地址
    • 配置网关的路由:给每个微服务设置路由信息

3. 示例

1.创建一个模块app-gateway,导入依赖

<dependencies>
    <!-- gateway -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    
    <!-- nacos-discovery -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
</dependencies>

2.创建引导类,开启注册服务

@EnableDiscoveryClient//开启注册服务
@SpringBootApplication
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}

3.编写配置文件

server:
  port: 10000
spring:
  application:
    name: api-gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #注册中心地址
    gateway:
      routes: #路由配置,是个数组
        - id: user-service # 路由id
          uri: lb://user-service #路由目的地的地址: lb 表示从注册中心拉取服务列表,并启用负载均衡
          predicates: #断言,什么样的请求可以到达目标地
            - Path=/user/**

4.测试

image-20240428162604062.png

四、Gateway断言

我们在配置文件中写的predicates断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转变为路由判断的条件。例如Path=/user/**是按照路径匹配,这个规则是由org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory类来处理的,像这样的断言工厂在SpringCloudGateway还有十几个,而我们需要掌握的只有Path

所有断言工厂的使用方式都是 在网关的路由配置中,使用predicates配置的:

spring:
  cloud:
    gateway:
      routes:
        - id: 路由唯一标识
          uri: lb://user-service #路由目的地的地址
          predicates: #断言,可以配置多个
            - Path=/user/** # - 断言名称=配置值

其它断言工厂参考:https://docs.spring.io/spring-cloud-gateway/docs/3.0.4/reference/html/#gateway-request-predicates-factories

名称 说明 示例 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 权重处理

五、Gateway过滤器

1. 网关登录校验

单体架构时我们只需要完成一次用户登录、身份校验,就可以在所有业务中获取到用户信息。而微服务拆分后,每个微服务都独立部署,不再共享数据。也就意味着每个微服务都需要做登录校验,这显然不可取。

登录是基于JWT来实现的,校验JWT的算法复杂,而且需要用到秘钥。如果每个微服务都去做登录校验,这就存在着两大问题:

  • 每个微服务都需要知道JWT的秘钥,不安全
  • 每个微服务重复编写登录校验代码、权限校验代码,麻烦

既然网关是所有微服务的入口,一切请求都需要先经过网关。我们完全可以把登录校验的工作放到网关去做,这样之前说的问题就解决了:

  • 只需要在网关和用户服务保存秘钥
  • 只需要在网关开发登录校验功能

image-20240428164142936.png

不过,这里存在几个问题:

  • 网关路由是配置的,请求转发是Gateway内部代码,我们如何在转发之前做登录校验?
  • 网关校验JWT之后,如何将用户信息传递给微服务?
  • 微服务之间也会相互调用,这种调用不经过网关,又该如何传递用户信息?

2. 网关过滤器

登录校验必须在请求转发到微服务之前做,否则就失去了意义。而网关的请求转发是Gateway内部代码实现的,要想在请求转发之前做登录校验,就必须了解Gateway内部工作的基本原理。

image-20240428164706865.png

如图所示:

  1. 客户端请求进入网关后由HandlerMapping对请求做判断,找到与当前请求匹配的路由规则(Route),然后将请求交给WebHandler去处理。
  2. WebHandler则会加载当前路由下需要执行的过滤器链(Filter chain),然后按照顺序逐一执行过滤器(后面称为**Filter**)。
  3. 图中Filter被虚线分为左右两部分,是因为Filter内部的逻辑分为prepost两部分,分别会在请求路由到微服务之前之后被执行。
  4. 只有所有Filterpre逻辑都依次顺序执行通过后,请求才会被路由到微服务。
  5. 微服务返回结果后,再倒序执行Filterpost逻辑。
  6. 最终把响应结果返回。

如图中所示,最终请求转发是有一个名为NettyRoutingFilter的过滤器来执行的,而且这个过滤器是整个过滤器链中顺序最靠后的一个。如果我们能够定义一个过滤器,在其中实现登录校验逻辑,并且将过滤器执行顺序定义到**NettyRoutingFilter**之前,这就符合我们的需求了!

那么,该如何实现一个网关过滤器呢?

网关过滤器链中的过滤器有两种:

  • GatewayFilter:路由过滤器,作用范围比较灵活,可以是任意指定的路由Route.
  • GlobalFilter:全局过滤器,作用范围是所有路由,不可配置。

3. GatewayFilter

应用到单个路由上,是局部过滤器,必须要配置到配置文件

它需要实现GatewayFilterFactory接口,并且需要在配置文件中配置才会生效;

GatewayFilter也可以配置为默认过滤器,针对所有路由进行过滤

所有的过滤器都可以参考官方手册 https://docs.spring.io/spring-cloud-gateway/docs/2.2.9.RELEASE/reference/html/#gatewayfilter-factories

局部过滤器

spring:
  cloud:
    gateway:
      default-filters:
        - 过滤器名称=配置的参数值
        - 过滤器名称=配置的参数值
      routes:
        - id: 路由id
          uri: 路由目的地lb://目标服务名
          predicates:
            - Path=/xxx/**
          filters:
            - 过滤器名称=配置的参数值
            - 过滤器名称=配置的参数值

配置过滤器示例

spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes:
        - id: user-service #用户服务的路由配置
          uri: lb://user-service
          predicates:
            - Path=/user/**
          filters:
            - AddResponseHeader=abc, user service is strong
        - id: order-service #订单服务的路由配置
          uri: lb://order-service
          predicates:
            - Path=/order/**
          filters:
            - AddResponseHeader=aaa, order service works great
      default-filters: #添加到这里的过滤器,对所有路由都生效
        - AddResponseHeader=company, itcast

4. GlobalFilter

应用到所有的路由上,是全局过滤器,不需要配置到配置文件

它不需要在配置文件中配置,只要实现GlobalFilter接口即可

自定义全局过滤器

使用步骤
  1. 创建一个类,类上加@Component
  2. 实现GlobalFilter接口,重写filter方法,在filter方法里实现过滤逻辑
  3. 实现Ordered接口,重写getOrder方法,如果需要设置过滤器执行顺序的话
示例
@Component
public class DemoGlobalFilter implements GlobalFilter , Ordered {
    //执行过滤的方法
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //如果要处理请求,就从exchange里获取request对象
        ServerHttpRequest request = exchange.getRequest();
        //  获取请求路径
        System.out.println("本次请求路径:" + request.getURI());
        //  获取请求头
        System.out.println("请求头Host:" + request.getHeaders().getFirst("Host"));
        //如果要处理响应,就从exchange里获取response对象
        ServerHttpResponse response = exchange.getResponse();
        //  设置响应状态码
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        //  设置响应cookie
        response.addCookie(ResponseCookie.from("cookieName", "cookieValue").build());
        //  结束本次请求,并返回响应
        // return response.setComplete();
        //放行
        return chain.filter(exchange);
    }

    //设置过滤器的执行顺序,值越小,执行的越早
    @Override
    public int getOrder() {
        return 0;
    }
}

六、跨域问题

1. 浏览器的同源策略

1.1 什么是同源策略

1995年,同源策略由 Netscape 公司引入浏览器。目前,所有浏览器都实行这个同源策略。它是指:一个页面,只允许访问与页面同源的某些资源。

  • 所谓的同源包含:同协议、同域名(同IP)、同端口。假如有一个资源是http://www.itcast.cn/a.html,那么:

    https://www.itcast.cn/user/1:不同源,因为协议不同

    http://itcast.cn/user/1:不同源,因为域名不同

    http://www.itcast.cn:81/user/1:不同源,因为端口不同

    http://www.itcast.cn/user/1:同源,因为同协议、同域名、同端口

  • 被同源限制的资源有:

    • Cookie、LocalStorage 和 IndexDB:只能同源的页面进行访问
    • DOM和js对象 :只能同源的页面才能获得
    • AJAX :只能向同源的资源发Ajax请求

1.2 什么是跨域问题

如果http://localhost:80/index.html页面上要发起一个Ajax请求,请求的目的地是:http://localhost:8080/user/1,这就是一个跨域Ajax请求了。

受限于浏览器的同源策略,这次请求是必定发送不成功的

但是目前流行的开发方式是前后端分离,即前端资源使用nginx部署到单独的服务器上,服务端项目部署到其它服务器上,这样的情况下,跨域请求就不可避免了。我们该如何规避浏览器的同源策略,允许浏览器跨域发送Ajax请求呢? 03.跨域问题.jpeg

2. 解决跨域问题

只需要在网关里添加如下配置即可:

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