SpringCloud微服务 【实用篇】| 统一网关Gateway
作者:mmseoamin日期:2023-12-27

目录

一:统一网关Gateway

1. 为什么需要网关

2. gateway快速入门

3. 断言工厂

4. 过滤器工厂

5. 全局过滤器

6. 跨域问题


一:统一网关Gateway

前面我们已经学习了注册中心Eureka、Nacos和配置管理中心Nacos;但是此时存在很多安全的问题,服务器摆在那里谁都可以进行访问!

1. 为什么需要网关

网关功能:

①身份认证和权限校验:微服务直接摆在那里允许任何人都可以访问,不太安全;需要进行身份验证,一切请求先到网关Gateway再到微服务,验证过后在进行放行!

②服务路由、负载均衡:放行过后,问题又来了,当用户放松请求处理业务时,网关肯定处理不了业务,需要把请求给对应的微服务;但是需要判断是发给order-service还是user-service进行处理?每一个微服务后面肯定有很多实例,所以还需要进行服务路由和负载均衡!

③请求限流:允许用户的请求量,限量;是对微服务的一种保护机制!

网关的技术实现:

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

①Gateway:SpringCloudGateway是基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能。

②Zuul:Zuul是基于Servlet的实现,属于阻塞式编程。

总结网关的作用:

①对用户请求做身份认证、权限校验; 

②将用户请求路由到微服务,并实现负载均衡 ;

③对用户请求做限流;

2. gateway快速入门

搭建网关服务的步骤:

第一步:创建新的module,引入SpringCloudGateway的依赖和nacos的服务发现依赖

注:这里需要Nacos依赖是因为也要把网关Gateway也注入注册中心Nacos里!

 

   org.springframework.cloud
   spring-cloud-starter-gateway



   com.alibaba.cloud
   spring-cloud-starter-alibaba-nacos-discovery 

第二步:服务的启动需要启动类

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);
    }
}

第三步:application.yml中编写路由规则配置及nacos地址

server:
  port: 10010 # 服务端口
spring:
  application:
    name: gateway # 服务名称
  cloud:
    nacos:
      server-addr: localhost:8848 # nacos服务地址
    gateway: # 服务路由配置
      routes: # 表示规则
        - id: userservice # 路由标识
          # uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
          uri: lb://user-service # 路由的目标地址 lb就是负载均衡,后面跟服务名称
          predicates:
            - Path=/user/** # 路径断言,判断是否以/user开头
        - id: orderservice
          uri: lb://order-service
          predicates:
            - Path=/order/**

启动服务,此时访问就不需要:http://localhost:8080/order/101 而是http://localhost:10010/order/101 这种形式

成功把把请求从网关路由到微服务!

原理剖析

①首先发起请求,端口是10010,而网关端口号也是10010,一定会进入网关!网关无法处理业务,只能基于路由规则进行判断(前面定义了两个路由规则)。

②根据路由规则匹配到的是user-service,然后就可以找到nacos注册中心进行服务拉取,再去负载均衡挑一个。

网关搭建步骤:

1. 创建项目,引入nacos服务发现和gateway依赖;

2. 配置application.yml,包括服务基本信息、nacos地址、路由;

路由配置包括:

1. 路由id:路由的唯一标示;

2. 路由目标(uri):路由的目标地址,http代表固定地址,lb代表根据服务名负载均衡;

3. 路由断言(predicates):判断路由的规则;

4. 路由过滤器(filters):对请求或响应做处理;(后面会讲)

3. 断言工厂

网关路由可以配置的内容包括:

①路由id:路由唯一标示;

②uri:路由目的地,支持lb和http两种;

③predicates:路由断言,判断请求是否符合要求,符合则转发到路由目的地;

④filters:路由过滤器,处理请求或响应;

路由断言工厂Route Predicate Factory

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

Spring提供了11种基本的Predicate工厂:

名称

说明

示例

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

权重处理

 详细的使用规则参考官网:Spring Cloud Gateway

注:如果此时路由规则不符合,浏览器页面包404错误!

增加时间路由规则:给order-service增加在2023后访问才符合规则

predicates:
    - Path=/order/**
    - After=2031-01-20T17:42:47.789-07:00[America/Denver] # 表明在2023年后访问符合

执行结果: 

1. PredicateFactory的作用是什么?

读取用户定义的断言条件,对请求进行解析并做出判断。

2. Path=/user/**是什么含义?

对请求对路进行解析,路径是以/user开头的就认为是符合的。

4. 过滤器工厂 

过滤器工厂 GatewayFilter

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

Spring提供了31种不同的路由过滤器工厂。例如:

更详细的可以参考官方网站:Spring Cloud Gateway

名称

说明

AddRequestHeader

给当前请求添加一个请求头

RemoveRequestHeader

移除请求中的一个请求头

AddResponseHeader

给响应结果中添加一个响应头

RemoveResponseHeader

从响应结果中移除有一个响应头

RequestRateLimiter

限制请求的流量

...

案例:给所有进入user-service的请求添加一个请求头Truth=Itcast is freaking awesome!

注:key和value之间是以 逗号 的方式连接!

验证执行结果:在UserController中使用@RequestHeader注解拿到请求头信息

    @GetMapping("/{id}")
    public User queryById(@PathVariable("id") Long id,
                          @RequestHeader(value = "Truth",required = false) String truth
                          ) {
        // 进行打印
        System.out.println("Truth: "+truth);
        return userService.queryById(id);
    }

思考:此时只是给某个微服务增加请求头信息,那么如果是所有的微服务都添加呢?

注:使用默认过滤器default-filter。配置的某一个微服务的过滤器,其filter在route的下面一级;而全局过滤器default-filter是与route同级!

5. 全局过滤器

全局过滤器 GlobalFilter

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

定义方式是实现GlobalFilter接口

exchange参数: 请求上下文,里面可以获取Request、Response等信息;
chain参数:过滤器链,用来把请求委托给下一个过滤器,放行;
Mono: 返回标示当前过滤器业务结束;

package org.springframework.cloud.gateway.filter;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
public interface GlobalFilter {
    Mono filter(ServerWebExchange exchange, GatewayFilterChain chain);
}

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

参数中是否有authorization,authorization参数值是否为admin,如果同时满足则放行,否则拦截!

在gateway启动类的同包下定义一个过滤器

①首先通过exchange参数获取到request对象,调用request对象的getQueryParams方法获取到所有的请求参数。然后从请求参数中通过authorization这个key获取value值admin。如果这个值存在:就调用chain执行链的filter方法,把exchange传下去;如果这个值不存在:就通过exchange参数获取到response对象,通过这个对象的setComplete方法进行拦截。在拦截之前还可以通过通过response方法设置状态码,增加用户的体验感!

②增加@Component注解组件扫描注解,纳入Spring的管理。

③增加@Order注解,顺序组件;将来可能会定义很对组件,这里是为了先执行。还可以实现Ordered接口,重写getOrder方法进行设置。

package cn.itcast.gateway;
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)
@Component
public class AuthrizeFilter implements GlobalFilter, Ordered {
    @Override
    public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 第一步:获取所有参数
        // 获取request对象
        ServerHttpRequest request = exchange.getRequest();
        // 获取所有请求参数
        MultiValueMap params = request.getQueryParams();
        // 第二步:根据authorization参数获取value值
        String auth = params.getFirst("authorization");
        // 第三步:判断请求参数的值是不是等于admin
        if ("admin".equals(auth)){
            // 是,放行
            return chain.filter(exchange);
        }
        // 不是,拦截
        // 结束之前设置状态码
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        return exchange.getResponse().setComplete();
    }
    // 实现Ordered接口的方法也可以
    @Override
    public int getOrder() {
        return -1;
    }
}

进行访问:如果不增加参数就会报401错误(未登录错误)

1. 全局过滤器的作用是什么?

对所有路由都生效的过滤器(这点和默认过滤器default-filter效果相同),并且可以自定义处理逻辑,比较灵活;

2. 实现全局过滤器的步骤?

①实现GlobalFilter接口;

②添加@Order注解或实现Ordered接口 和 添加组件扫描@Component注解;

③编写处理逻辑;

过滤器执行顺序

前面已经讲解了三个过滤器:路由过滤球、默认的过滤器、全局过滤器;接下来就分析一下这三个过滤器的执行顺序!

注:请求进入网关会碰到三类过滤器:当前路由的过滤器、DefaultFilter、GlobalFilter请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器

思考1:从目前来看,这三个过滤器不是同一种类型,怎么能放到同一个集合当中呢?

1. 对于路由过滤器和默认过滤器default-filter从配置文件来看配置方式相同:

其本质上实际都是AddRequestHeaderGatewayFilterFactory对象!这个过滤器的工厂就会读取配置文件生成一个真正的过滤器GatewayFilter;所以路由过滤器默认过滤器都是同一类:GatewayFilter!

2. 在FilteringWebHandler类里面有一个FilteringWebHandler(过滤器适配器)这个类适配器实现了GatewayFilter接口,在适配器内部又接收了一个全局过滤器参数GlobalFiter;通过适配器模式进行传参当做GatewayFilter来使用,这样就建立了联系!

所以可以认为这三种过滤器都是GatewayFilter类型,同一种类型就可以放到List集合当中进行排序!

思考2:这样新的问题就引出来了,怎么进行排序呢?

①我们已经知道,每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前。

②对于全局过滤器GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值,由我们自己指定。

③对于路由过滤器和默认过滤器我们并没有去指定顺序!路由过滤器和默认过滤器defaultFilter的order由Spring指定,默认是按照声明顺序从1递增。例如:

各排各的: 

④如果此时过滤器的order值都是1怎么办呢?

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

详情可以参考源码:很清晰,可以自己看一下

①org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#getFilters()方法是先加载defaultFilters,然后再加载某个route的filters,然后合并。②org.springframework.cloud.gateway.handler.FilteringWebHandler#handle()方法会加载全局过滤器,与前面的过滤器合并后根据order排序,组织过滤器链。

6. 跨域问题

跨域问题处理

在微服务当中,所有的请求都要先经过网关,在到微服务;这样就不要在每个微服务进行处理,只需要在网关中进行处理!

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

域名不同: www.taobao.com 和 www.taobao.org 和 www.jd.com 和 miaosha.jd.com

域名相同,端口不同:localhost:8080和localhost8081

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

解决方案:CORS(浏览器去询问服务器的方式)

通过axios发送get请求,请求地址就是网关的地址




    
    
    
    Document


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

以8090端口进行运行,此时在控制台就可以看到报错请求:

进行配置

server:
  port: 10010 # 服务端口
spring:
  application:
    name: gateway # 服务名称
  cloud:
    nacos:
      server-addr: localhost:8848 # nacos服务地址
    gateway: # 服务路由配置
      routes:
        - id: userservice # 路由标识
          # uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
          uri: lb://user-service # 路由的目标地址 lb就是负载均衡,后面跟服务名称
          predicates:
            - Path=/user/** # 路径断言,判断是否以/user开头
#         filters:
#            - AddRequestHeader=Truth,Itcast is freaking awesome! # 注意key和value之间是以逗号隔开
        - id: orderservice
          uri: lb://order-service
          predicates:
            - Path=/order/**
            - Before=2031-01-20T17:42:47.789-07:00[America/Denver]
      default-filters:
        - AddRequestHeader=Truth,Itcast is freaking awesome! # 注意key和value之间是以逗号隔开
      globalcors: # 全局的跨域处理
        add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题(防止CORS浏览器询问服务器拦截)   corsConfigurations:
        corsConfigurations:
          '[/**]': # 拦截所有的请求
            allowedOrigins: # 允许哪些网站的跨域请求
              - "http://localhost:8090"
              - "http://www.leyou.com"
            allowedMethods: # 允许的跨域ajax的请求方式
              - "GET"
              - "POST"
              - "DELETE"
              - "PUT"
              - "OPTIONS"
            allowedHeaders: "*" # 允许在请求中携带的头信息
            allowCredentials: true # 是否允许携带cookie
            maxAge: 360000 # 这次跨域检测的有效期,有效期内直接放行

 重启网关,此时再次以8090端口发送请求就可以跨域访问啦!