跳过正文
  1. 文章/
  2. Java/
  3. SpringFramework/
  4. SpringCloud/

6、服务网关

·7206 字·15 分钟· loading · loading · ·
Java SpringFramework SpringCloud
GradyYoung
作者
GradyYoung
目录
SpringCloud - 点击查看当前系列文章
§ 6、服务网关 「 当前文章 」

微服务网关
#

  • 有一些问题:不同的微服务一般会有不同的网络地址,客户端在访问这些微服务时必须记住几十甚至几百个地址,这对于客户端方来说太复杂也难以维护。

  • 如果让客户端直接与各个微服务通讯,可能会有很多问题:

    • 客户端会请求多个不同的服务,需要维护不同的请求地址,增加开发难度
    • 加大身份认证的难度,每个微服务需要独立认证
  • 因此,我们需要一个微服务网关,介于客户端与服务器之间的中间层,所有的外部请求都会先经过微服务网关。客户端只需要与网关交互,只知道一个网关地址即可,这样简化了开发还有以下优点:

    • 易于监控
    • 易于认证
    • 减少了客户端与各个微服务之间的交互次数

什么是微服务网关
#

  • API网关是一个服务器,是系统对外的唯一入口

  • API网关封装了系统内部架构,为每个客户端提供一个统一定制的API(应用编程接口)

  • API网关方式的核心要点是,所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有的非业务功能

  • 通常,网关也是提供REST/HTTP的访问API。服务端通过API-GW注册和管理服务

微服务网关产品
#

  • Kong

    • 基于Nginx+Lua开发,性能高,稳定,有多个可用的插件(限流、鉴权等等)可以开箱即用。
    • 问题:只支持Http协议;二次开发,自由扩展困难;提供管理API,缺乏更易用的管控、配置方式
  • Zuul

    • Netflix开源,功能丰富,使用JAVA开发,易于二次开发;需要运行在web容器中,如Tomcat
    • 问题:缺乏管控,无法动态配置;依赖组件较多;处理Http请求依赖的是Web容器,性能不如Nginx
  • Traefik

    • Go语言开发;轻量易用;提供大多数的功能:服务路由,负载均衡等等;提供WebUI
    • 问题:二进制文件部署,二次开发难度大;UI更多的是监控,缺乏配置、管理能力
  • Spring Cloud Gateway

    • SpringCloud提供的网关服务
    • Nginx + lua实现
    • 使用Nginx的反向代理和负载均衡可实现对api服务器的负载均衡及高可用
    • 问题:自注册的问题和网关本身的扩展性

Zuul网关技术
#

  • Zuul是Netflix开源的微服务网关,它可以和Eureka、Ribbon、Hystrix等组件配合使用,Zuul组件的核心是一系列的过滤器。Spring Cloud对Zuul进行了整合和增强

  • Zuul的最主要作用:路由分配,过滤器(身份认证,铭感词处理……)

  • 当然,这个技术,跟Eureka,Hystrix-DashBoard一样又是项目经理的东西

  • 配一次,一般就不需要再做多次的修正

  • 网关程序,一般都是一个独立的微服务程序

配置方法
#

  • Zuul实际上,它是可以和Eureka配合起来使用的
  • Zuul支持与Eureka整合开发,根据ServiceID自动的从注册中心中获取服务地址并转发请求,这样做的好处不仅可以通过单个端点来访问应用的所有服务,而且在添加或移除服务实例的时候不用修改Zuul的路由配置

1、创建网关微服务,添加依赖
#

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!-- 导入Eureka的客户端依赖包 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

2、启动类添加eureka客户端、启动网关服务、springboot启动类注解
#

@SpringCloudApplication
@EnableZuulProxy
public class ZuulApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulApplication.class,args);
    }
}

3、配置application.yml文件,添加其他微服务路由
#

#配置端口
server:
  port: 10000
#配置项目名称
spring:
  application:
    name: zuul
#配置eureka注册路径
eureka:
  client:
    service-url:
      defaultZone: http://localhost:9000/eureka
#配置路由
zuul:
  routes:
    #路由ID/名称(可以随便写)
    micro-base-server:
      #访问路径(不能随便写,它需要对应微服务中Controller发布的接口)
      path:
        /auction/**
        /market/**
      #在eureka中注册的微服务名称
      serverId: micro-base-server
    micro-user-server:
      path:
        /feign/**
        /rent/**
      serverId: micro-user-server

4、进行访问
#

url格式:网关微服务ip:网关微服务端口/路由ID/对应微服务Controller路径

image-20210819102736505

5、路由配置简化
#

zuul:
  routes:
  	#eureka中微服务id: 访问路径
    micro-base-server: /auction/**,/market/**
    micro-user-server: /feign/**,/rent/**

过滤器
#

Zuul网关过滤器
#

  • Zuul它包含了两个核心功能:对请求的路由和过滤

  • 其中路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入口的基础;而过滤器功能则负责对请求的处理过程进行干预,是实现请求校验、服务聚合等功能的基础。其实,路由功能在真正运行时,它的路由映射和请求转发同样也由几个不同的过滤器完成的。所以,过滤器可以说是Zuul实现API网关功能最为核心的部件,每一个进入Zuul的HTTP请求都会经过一系列的过滤器处理链得到请求响应并返回给客户端

  • Zuul中的过滤器跟我们之前使用的javax.servlet.Filter不一样,javax.servlet.Filter只有一种类型,可以通过配置urlPatterns来拦截对应的请求

  • Zuul中的过滤器总共有4种类型,且每种类型都有对应的使用场景

过滤的结构图
#

img

  • PRE:这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等
  • ROUTING:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用Apache HttpClient或Netfilx Ribbon请求微服务
  • POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP
  • Header:收集统计信息和指标、将响应从微服务发送给客户端等
  • ERROR:在其他阶段发生错误时执行该过滤器

PRE过滤器范例
#

  • Pre过滤器的适用场景:身份认证,脱敏处理,编码集的统一设置等
  • 编写过滤器继承ZuulFilter,并且添加@Component注解
//需要被spring容器管理
@Component
public class MyZuulFilter extends ZuulFilter {

    /**
     * 用来指定过滤器的类型
     * pre:路由之前
     * routing:路由之时
     * post: 路由之后
     * error:发送错误调用
     */
    public String filterType() {
        return "pre";
    }
    /**
     * 过滤器的顺序
     * @return 返回的Int越小,越先执行
     * 每个过滤器,默认都有一个中间值5
     */
    public int filterOrder() {
        return 0;
    }
    /**
     * 用来判断哪些路径,是需要做过滤处理的
     * @return true 需要做过滤(就是需要执行run())  false 不需要做过滤(就是不需要执行run())
     */
    public boolean shouldFilter() {
        return true;
    }
    /**
     * 具体的过滤器规则
     */
    public Object run() throws ZuulException {
        RequestContext context = RequestContext.getCurrentContext();
        HttpServletRequest request = context.getRequest();
        System.out.println("进入路由的过滤器 "+request.getServletPath());
        //true:成功响应,false:拒绝响应
        context.setSendZuulResponse(true);
        context.setResponseStatusCode(200);
        return null;
    }
}

Gateway(SpringCloud)
#

Gateway的特点
#

  • 优点
    • 性能强劲,是第一代网关Zuul的1.6倍
    • 功能强大,内置了很多使用的功能,例如转发、监控、限流等
    • 设计优雅,容易扩展
  • 缺点
    • 其实现依赖于Netty与WebFlux,不是传统的Servlet编程模型,学习成本高
    • 不能将其部署在tomcat、jetty等servlet容器中,只能打成jar包执行
    • 需要Spring Boot 2.0以上的版本,才支持

简单使用
#

1、新建modules,添加依赖
#

不要添加spring-boot-starter-web,因为gateway使用的是webFlux,和springboot的web场景启动器会有冲突

<dependencies>
    <dependency>
        <groupId>top.ygang</groupId>
        <artifactId>star-common</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.csp</groupId>
        <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
    </dependency>
</dependencies>

2、添加启动类
#

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

3、编写配置文件
#

server:
  port: 80
spring:
  application:
    name: gateway
  cloud:
    gateway:
      # 自定义配置路由规则
      routes:
        # 路由标识,必须唯一,默认是UUID
        - id: student-sys
          # 路由的目标地址
          uri: http://localhost:8080
          # 路由的优先级,数字越小代表路由的优先级越高
          order: 1
          # 断言
          predicates:
            # 路径断言,当请求路径匹配Path时,就会路由到uri
            - Path=/student-sys/**
          # 过滤器
          filters:
            # 转发之前,去掉1层路径
            - StripPrefix=1

4、进行访问
#

访问地址格式:http://gateway的ip:gateway的端口/请求路径Path/Controller路径

image-20210824120523201

Gateway和nacos结合
#

在简单使用的基础上,将Gateway添加到nacos上,通过微服务名称在nacos上查找微服务,一边实现访问微服务集群,实现负载均衡

1、添加依赖
#

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

2、启动类添加注解
#

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

3、修改配置文件
#

server:
  port: 80
spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        # 将服务注册到nacos
        server-addr: http://localhost:8848
    gateway:
      discovery:
        locator:
          # 从nacos中获取服务信息,自动生成routes,开启会导致断言失效
          enabled: false
      # 自定义配置路由规则
      routes:
        # 路由标识,必须唯一,默认是UUID
        - id: student-sys
          # 路由的目标地址,lb(loadblance)表示使用负载均衡,其后是微服务的标识
          uri: lb://student-system
          # 路由的优先级,数字越小代表路由的优先级越高
          order: 1
          # 断言
          predicates:
            # 路径断言,当请求路径匹配Path时,就会路由到uri
            - Path=/student-sys/**
          # 过滤器
          filters:
            # 转发之前,去掉1层路径
            - StripPrefix=1

nacos配置的精简版
#

server:
  port: 80
spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        # 将服务注册到nacos
        server-addr: http://localhost:8848
    gateway:
      discovery:
        locator:
          # 让gateway从nacos中获取服务信息,自动生成routes,开启会导致断言失效
          enabled: true
  • 精简版中没有关于路由的配置routes,不需要自定义配置路由规则
    • uri就等于 lb://微服务名称
    • path就等于 /微服务名称/**
  • 精简版是不常用的,因为只实现了路由功能。

Gateway核心架构
#

路由(Route)是Gateway中的组件之一,用来选择某一个具体的微服务。路由有以下的相关配置:

  • id,路由标识符,区别于其他Route
  • uri,路由选择的目标uri,即客户的请求最终被转发到的微服务
  • order,用于多个Route之间的排序,数值越小优先级越高
  • predicate,断言的作用是进行条件判断,只有断言都返回真,才会真正地执行路由
  • filter,过滤器,用于修改请求和响应信息

image-20200925113929580

大致的执行流程如下:

  1. Gateway Client向Gateway Server发送请求
  2. 请求首先会被HttpWebHandlerAdapter进行提取,进而组装成网关上下文
  3. 网关上下文被传递到DispatcherHandler,DispatcherHandler负责先找到能处理该请求的映射器处理器,这里它找到的映射器处理器是RoutePredicateHandlerMapping(没有分发给其他的HandlerMapping,是因为这个请求已经被封装进网关上下文了,该请求不是一般的请求,只能由特定的HandlerMapping处理,也就是RoutePredicateHandlerMapping)
  4. 路由断言处理映射器(RoutePredicateHandlerMapping)主要用于路由的查找,如何找呢?就是根据断言来找。
  5. 如果断言成功,由FilerWebHandler创建过滤器链并调用
  6. 请求会依次经过PreFilter、微服务、PostFilter的方法,最终返回响应

predicates断言
#

Predicate(断言)用于进行条件判断,只有断言都返回真,才会执行真正的路由。

内置路由断言工厂
#

Spring Cloud Gateway包括许多内置的断言工厂,所有这些断言都与HTTP请求的不同属性匹配。具体如下:

  • 基于Datetime类型的断言工厂

    • AfterRoutePredicateFactory:接收一个日期参数,判断请求日期是否晚于指定日期

    • BeforeRoutePredicateFactory:接收一个日期参数,判断请求日期是否早于指定日期

    • BetweenRoutePredicateFactory:接收两个日期参数,判断请求日期是否介于指定时间段之内

    • -After=2020-12-31T23:59:59.999+08:00[Asia/Shanghai]

  • 基于远程地址的断言工厂

    • RemoteAddrRoutePredicateFactory:接收一个IP地址段,判断请求主机地址是否在地址段中

    • -RemoteAddr=192.168.1.1/24

  • 基于Cookie的断言工厂

    • CookieRoutePredicateFactory:接收两个参数,cookie名字和一个正则表达式。判断请求Cookie是否具有给定名称且值与正则表达式匹配

    • -Cookie=foo,\w*

  • 基于Header的断言工厂

    • HeaderRoutePredicateFactory:接收两个参数,标题名和正则表达式。判断请求Header是否具有给定名称且值与正则表达式匹配

    • -Header=X-Request-Id,\d+

  • 基于Method请求方法的断言工厂

    • MethodRoutePredicateFactory:接收一个参数,判断请求类型是否与指定的类型匹配

    • -Method=Get

  • 基于Path请求路径的断言工厂

    • PathRoutePredicateFactory:接收一个参数,判断请求的URI部分是否满足路径规则

    • -Path=/foo/bar

  • 基于Host的断言工厂

    • HostRoutePredicateFactory:接收一个参数,该参数是主机名模式。判断请求的Host是否满足匹配规则。
  • 基于Query请求参数的断言工厂

    • QueryRoutePredicateFactory:接收两个参数,请求参数和正则表达式,判断请求参数是否具有指定的名称且值与正则表达式匹配
  • 基于路由权重的断言工厂

    • WeightRoutePredicateFactory:接收一个【组名,权重】,然后对同一个组内的路由按照权重转发

自定义断言工厂
#

1、编写一个断言工厂的类

类名必须以RoutePredicateFactory为结束

继承AbstractRoutePredicateFactory

@Component
public class AgeRoutePredicateFactory extends AbstractRoutePredicateFactory<AgeRoutePredicateFactory.Config> {
    public AgeRoutePredicateFactory(){
        super(AgeRoutePredicateFactory.Config.class);
    }

    // 读取配置文件中的参数,并封装到Config内部类的对象中
    @Override
    public List<String> shortcutFieldOrder() {
        // 该集合中元素的顺序,必须与配置文件中参数的顺序一致
        return Arrays.asList("minAge", "maxAge");
    }

    @Override
    public Predicate<ServerWebExchange> apply(AgeRoutePredicateFactory.Config config) {

        return new Predicate<ServerWebExchange>(){

            @Override
            public boolean test(ServerWebExchange serverWebExchange) {
                //通过路由获得传递的参数
                String age = serverWebExchange.getRequest().getQueryParams().getFirst("age");
                //判断是否有值
                if(!StringUtils.isEmpty(age)){
                    //转型
                    Integer a = Integer.parseInt(age);
                    return a>=config.getMinAge()&&a<=config.getMaxAge();
                }
                return false;
            }
        };
    }

    public static class Config {
        private Integer minAge;
        private Integer maxAge;
        public Integer getMinAge() {
            return minAge;
        }

        public void setMinAge(Integer minAge) {
            this.minAge = minAge;
        }

        public Integer getMaxAge() {
            return maxAge;
        }

        public void setMaxAge(Integer maxAge) {
            this.maxAge = maxAge;
        }
    }
}

2、在配置文件中配置断言

image-20210824165406744

配置后,在启动gateway时,会对自定义断言工厂进行加载

image-20210824165628160

访问时,如果没有传age参数或age不符合配置中的范围,那么会404

image-20210824165822083

filters过滤器
#

过滤器的描述
#

过滤器的作用:在请求的处理过程中,对请求和响应进行加工。

在Gateway中,Filter的生命周期主要有两个阶段:“pre"和"post”

  • PRE:这种过滤器在请求被路由之前调用。我们可以利用这种过滤器实现身份验证、记录调试信息等。
  • POST:这种过滤器在请求被路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息等等。

Gateway的Filter从作用范围可分为两种:GatewayFilter与GloalFilter。

  • GatewayFilter:应用到单个路由或者一个分组的路由上
  • GlobalFilter:应用到所有的路由上

局部过滤器
#

内置局部过滤器
#
过滤器工厂 作用 参数
AddRequestHeader 为原始请求添加Header Header的名称和对应的值
AddRequestParameter 为原始请求添加请求参数 参数名和对应的值
AddResponseHeader 为原始响应添加Header Header的名称和对应的值
DedupeResponseHeader 剔除响应头中重复的值 需要去重的Header名称及去重策略
Hystrix 为路由引入Hystrix的断路器保护 HystrixCommand的名称
FallbackHeaders 为FallbackUrl的请求头中添加具体的异常信息 Header的名称
PrefixPath 为原始请求路径添加前缀 前缀路径
PreserveHostHeader 为请求添加一个preserveHostHeader=true的属性,
路由过滤器会检查该属性以决定是否要发送原始的Host
RequestRateLimiter 用于对请求限流,限流算法为令牌桶 keyResolver、rateLimiter、statusCode、
denyEmptyKey、emptyKeyStatus
RedirectTo 将原始请求重定向到指定的URL http状态码及重定向的url
RemoveRequestHeader 为原始请求删除某个Header Header名称
RemoveResponseHeader 为原始响应删除某个Header Header名称
RewritePath 重写原始的请求路径 原始路径正则表达式以及重写后的路径
RewriteResponseHeader 重写原始响应中的某个Header Header名称,值的正则表达式,重写后的值
SaveSession 在转发请求之前,强制执行WebSession::save操作
secureHeaders 为原始响应添加一系列响应头,这些响应头可以保证安全 无,支持修改这些安全响应头的值
SetPath 修改原始的请求路径 修改后的路径
SetResponseHeader 修改原始响应中,某个Header的值 Header名称,修改后的值
SetStatus 修改原始响应的状态码 HTTP状态码,而已是数字,也可以是字符串
StripPrefix 用于截断原始请求的路径 使用数字表示要截断的路径的数量
Retry 针对不同的响应进行重试 retries、statuses、methods、series
RequestSize 设置允许接收的请求的最大大小,如果请求包大小超过设置的
值,则会返回413 Payload Too Large
请求包大小,单位为字节,默认值为5M
ModifyRequestBody 在转发请求之前修改原始请求体内容 修改后的请求体内容
ModifyResponseBody 修改原始响应体的内容 修改后的响应体内容
内置局部过滤器的使用
#
server:
  port: 9009
spring:
  application:
    name: star-gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    gateway:
      discovery:
        locator:
          enabled: false
      routes:
        - id: product_route
          uri: lb://star-product
          order: 1
          predicates:
            - Path=/product-serv/**
          filters:
            - StripPrefix=1
            - SetStatus=202				#修改 response的状态码

请求后,相应的状态码变为202

image-20210824170324246

自定义局部过滤器
#

1、编写一个过滤器类

类名必须以GatewayFilterFactory为结尾

@Component
public class LogGatewayFilterFactory extends AbstractGatewayFilterFactory<LogGatewayFilterFactory.Config> {
    public LogGatewayFilterFactory(){
        super(LogGatewayFilterFactory.Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        return new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                if(config.isConsolelog()){
                    System.out.println("控制台日志功能开启!!");
                }
                if(config.isCachelog()){
                    System.out.println("缓存日志功能开启!!");
                }
                return chain.filter(exchange);
            }
        };
    }

    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList("consoleLog", "cacheLog");
    }

    public static class Config {
        private boolean consolelog;
        private boolean cachelog;

        public boolean isConsolelog() {
            return consolelog;
        }

        public void setConsolelog(boolean consolelog) {
            this.consolelog = consolelog;
        }

        public boolean isCachelog() {
            return cachelog;
        }

        public void setCachelog(boolean cachelog) {
            this.cachelog = cachelog;
        }
    }
}

2、在配置文件中添加过滤器

image-20210824170820513

全局过滤器
#

全局过滤器不需要再配置文件中进行配置

内置全局过滤器
#
全局过滤器 作用
ForwardRoutingFilter 用于本地forward,也就是将请求在Gateway服务内进行转发,而不是转发到下游服务
LoadBalancerClientFilter 整合Ribbon实现负载均衡
NettyRoutinFilter 使用Netty的 HttpClient 转发http、https请求
NettyWriteResponseFilter 将代理响应写回网关的客户端侧
RouteToRequestUrlFilter 将从request里获取的原始url转换成Gateway进行请求转发时所使用的url
WebsocketRoutingFilter 使用Spring Web Socket将转发 Websocket 请求
GatewayMetricsFilter 整合监控相关,提供监控指标
自定义全局过滤器
#

1、编写过滤器类,实现GlobalFilter和Ordered接口,重写方法

@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String token = exchange.getRequest().getQueryParams().getFirst("token");
        if(!"test".equals(token)){
            System.out.println("未认证");
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

2、进行访问,只有参数token为test时,才可以正常访问

image-20210824174345639

image-20210824174359375

SpringCloud - 点击查看当前系列文章
§ 6、服务网关 「 当前文章 」