服务网关Gateway

1.1.为什么要使用Gateway?

​ 大家都都知道在微服务架构中,一个系统会被拆分为很多个微服务。那么作为客户端要如何去调用这么多的微服务呢?如果没有网关的存在,我们只能在客户端记录每个微服务的地址,然后分别去调用。这样的架构,会存在着诸多的问题:

  1. 客户端请求不同的微服务,就要维护不同的ip

  2. 客户端无法实现负载均衡

上面的这些问题可以借助API网关来解决:

在业界比较流行的网关,有下面这些:

  • Ngnix+lua

    使用nginx的反向代理和负载均衡可实现对api服务器的负载均衡及高可用 lua是一种脚本语言,可以来编写一些简单的逻辑,nginx支持lua脚本

  • Spring Cloud Gateway

    Spring公司为了替换Zuul而开发的网关服务,将在下面具体介绍。

注意:SpringCloud alibaba技术栈中并没有提供自己的网关,我们可以采用Spring Cloud Gateway来做网关。

1.2.Gateway介绍

1.2.1.Gateway是什么

​ Spring Cloud Gateway是Spring公司基于Spring 5.0,Spring Boot 2.0 和 Project Reactor等技术开发的网关,目标是替代 Netflflix ZUUL,并且基于 Filter 链的方式提供了路由,过滤,和限流等功能。

组件RPS(request per second)
Spring Cloud GatewayRequests/sec: 32213.38
Zuul1XRequests/sec: 20800.13

上表为Spring Cloud Gateway与Zuul的性能对比,从结果可知,Spring Cloud Gateway的RPS是Zuul的1.6倍

1.2.3.Gateway执行流程

  1. Gateway Client向Gateway Server发送请求

  2. HandlerMapping负责路由查找,并根据路由断言判断路由是否可用

  3. WebHandler创建过滤器链并调用

  4. 请求会一次经过PreFilter–微服务–PostFilter的方法,最终返回响应给Gateway Client

1.2.创建gateway工程

1.2.1.创建工程

1.2.1.pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springcloud_parent</artifactId>
        <groupId>com.bjpowernode</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>api_gateway</artifactId>

    <dependencies>
        <!-- nacos启动器 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!-- gateway的启动器,gateway有自己的服务器。多个Tomcat会报错 -->
        <!--注意:不要添加spring-boot-starter-web启动器-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
    </dependencies>
</project>

注意:不要添加spring-boot-starter-web启动器

        <!--servlet运行的服务器是tomcat-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

1.2.1.application.yml

spring:
  application:
    name: api-gateway
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.239.131:8848
server:
  port: 9527

1.2.1.App

package com.bjpowernode;

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

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

1.3.路由

1.3.1.通过断言条件路由

1.3.1.1.application.yml
spring:
  cloud:
    gateway:
      routes:
        - id: sentinel-consumer #自定义的路由id,要求保持唯一即可
          uri: http://127.0.0.1:80 # 请求要转发到的地址
          predicates: #断言,断言通过才能有下一步的路由功能
            - Path=/consumer/**  #只有断言条件返回true(请求路径包含“/consumer”)时,才进行路由转发

1.3.1.2. 测试

1.开启服务和网关:

  • api_gateway
  • sentinel_consumer
  • sentinel_provider

2.浏览器访问:http://127.0.0.1:9527/consumer/getUserById/6

在这里插入图片描述

1.3.2.通过服务名路由

1.3.2.1.application.yml
spring:
  cloud:
    gateway:
      routes:
        - id: sentinel-consumer #自定义的路由ID,保持唯一
          uri: lb://sentinel-consumer #lb代表从注册中心获取服务
          predicates: #断言
            - Path=/consumer/** #只有断言条件返回true(请求路径包含“/consumer”)时,才进行路由转发
1.3.2.2. 测试
  1. 重启网关:
  • api_gateway
  1. 浏览器访问:http://127.0.0.1:9527/consumer/getUserById/3
    在这里插入图片描述

1.4.断言

1.4.1.内置断言工厂

Spring Cloud Gateway 的功能很强大,前面我们只是使用了 predicates 进行了简单的条件匹配,其实Spring Cloud Gataway 帮我们内置了很多 Predicates 功能。

可参考spring官网:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gateway-request-predicates-factories

1.4.1.1.基于Datetime类型的断言工厂
  • 说明

此类型的断言根据时间做判断,主要有三个:

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

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

BetweenRoutePredicateFactory:接收两个日期参数,判断请求日期是否在指定时间段内。

  • 案例(均可重启后自测)
- After=2022-04-09T17:20:54.957+08:00[Asia/Shanghai]
1.4.1.2.基于RemoteAddr的断言工厂
  • 说明

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

  • 案例
- RemoteAddr=192.168.1.1/24
1.4.1.3.基于Cookie的断言工厂
  • 说明

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

  • 案例
- Cookie=chocolate, ch.
1.4.1.4.基于Header的断言工厂
  • 说明

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

  • 案例
- Header=X-Request-Id, \d+ 
1.4.1.3.基于Host的断言工厂
  • 说明

HostRoutePredicateFactory:接收一个参数,主机名模式。判断请求的Host是否满足匹配规则。

  • 案例
- Host=**.testhost.org 
1.4.1.5.基于Method请求方法的断言工厂
  • 说明

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

  • 案例
- Method=GET 
1.4.1.6.基于Path请求路径的断言工厂
  • 说明

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

  • 案例
- Path=/foo/{segment}
1.4.1.7.基于Query请求参数的断言工厂
  • 说明

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

  • 案例
- Query=baz, ba.

1.4.2.自定义路由断言工厂

1.4.2.1.内置断言工厂的实现原理

我们来设定一个场景: 假设我们的应用仅仅让age>18的人来访问,在自定义断言工厂之前,我们先看内置断言工厂的实现原理,打开AfterRoutePredicateFactory这个内置断言工厂:

在这里插入图片描述

1.4.2.2.创建断言工厂(第一步自定义断言工厂类)
package com.bjpowernode.PredicateFactory;

import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.lang.StringUtils;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;

import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;

/**
 * 自定义断言工厂
 * Config 是一个类  需要我们自己去定义
 */
@Component
public class AgeRoutePredicateFactory extends AbstractRoutePredicateFactory<AgeRoutePredicateFactory.Config> {

    public AgeRoutePredicateFactory() {
        super(AgeRoutePredicateFactory.Config.class);
    }

    //读取配置文件的中参数值 给他赋值到配置类中的属性上
    @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) {
                //1 接收前台传入的age参数
                String ageStr = 
                    serverWebExchange.getRequest().getQueryParams().getFirst("age");
                //2 先判断是否为空
                if (StringUtils.isNotEmpty(ageStr)) {
                    //3 如果不为空,再进行路由逻辑判断
                    int age = Integer.parseInt(ageStr);
                    if (age < config.getMaxAge() && age > config.getMinAge()) {
                    //4.通过判断则放行
                        return true;
                    } else {
                        return false;
                    }
                }
                return false;
            }
        };
    }

    //自定义一个配置类, 用于接收配置文件中的参数
    @Data
    @NoArgsConstructor
    public static class Config {
        private int minAge;//18
        private int maxAge;//60
    }
}
1.4.2.3.配置断言(第二步application.yml配置文件)
spring:
  cloud:
    gateway:
      routes:
        - id: sentinel-consumer
          uri: lb://sentinel-consumer
          predicates:
            - Path=/consumer/**
            - Age=18,60 #配置断言必须使用断言工厂类名的前缀
1.4.2.4.测试
  1. 浏览器访问:http://127.0.0.1:9527/consumer/getUserById/1?age=17

在这里插入图片描述

  1. 浏览器访问:http://127.0.0.1:9527/consumer/getUserById/1?age=19
    在这里插入图片描述

1.5.过滤

Spring Cloud Gateway提供了过滤器的功能,可以对进入网关的请求响应做处理:

在这里插入图片描述

Spring Cloud Gateway 的 Filter 从作用范围可分为另外两种GatewayFilter 与 GlobalFilter。

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

  • GlobalFilter:应用到所有的路由上。

1.5.1.内置过滤器工厂

Spring Cloud Gateway中通过GatewayFilter的形式内置了很多不同类型的局部过滤器,可参考spring官网:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gatewayfilter-factories

过滤器工厂作用
AddRequestHeader给当前请求添加一个请求头
AddRequestParameter为原始请求添加请求参数
AddResponseHeader给响应结果中添加一个响应头
DedupeResponseHeader去掉重复请求头
Spring Cloud CircuitBreaker断路器
FallbackHeaders添加熔断后的异常信息到请求头
MapRequestHeader将上游请求头的值赋值到下游请求头
PrefixPath匹配的路由添加前缀
PreserveHostHeader保留原请求头
RequestRateLimiter限制请求的流量
RedirectTo重定向
RemoveRequestHeader移除请求中的一个请求头
RemoveResponseHeader从响应结果中移除有一个响应头
RemoveRequestParameter移除请求参数
RewritePath重写路径
RewriteLocationResponseHeader重写响应头中Location的值
RewriteResponseHeader重写响应头
SaveSession向下游转发请求前前置执行WebSession::save的操作
SecureHeaders禁用默认值
SetPath设置路径
SetRequestHeader重置请求头
SetResponseHeader修改响应头
SetStatus修改响应的状态码
StripPrefix对指定数量的路径前缀进行去除
Retry重试
RequestSize请求大小大于限制时,限制请求到达下游服务
SetRequestHostHeader重置请求头值
Modify a Request Body修改请求体内容
Modify a Response Body修改响应体内容
Relay将 OAuth2 访问令牌向下游转发到它所代理的服务
CacheRequestBody在请求正文发送到下游之前缓存请求正文并从 exchagne 属性获取正文

1.5.2.自定义过滤器工厂

需求:记录调用远程服务所需要的时间

1.5.2.1.创建过滤器工厂
package com.bjpowernode.FilterFactory;

import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Arrays;
import java.util.Date;
import java.util.List;
/**
 * 过滤器链
 */
@Component
public class LogGatewayFilterFactory
        extends AbstractGatewayFilterFactory<LogGatewayFilterFactory.Config> {

    public LogGatewayFilterFactory() {
        super(Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        return new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, 
                                     GatewayFilterChain chain) {
                if (config.getLog()) {
                    long beginTime = System.currentTimeMillis();
                    return chain.filter(exchange).then(Mono.fromRunnable(new Runnable() {
                        @Override
                        public void run() {
                            long endTime = System.currentTimeMillis();
                            System.out.println("time of call service :" + 
                                               			(endTime - beginTime));
                        }
                    }));
                }
                //不通过不放行
                return null;
            }
        };
    }

    //读取配置文件中的参数 赋值到配置类中
    @Override
    public List<String> shortcutFieldOrder() {
        //这里的顺序要跟配置文件中的参数顺序一致
        return Arrays.asList("log");
    }

    //自定义一个配置类, 用于接收配置文件中的参数
    public static class Config {
        private boolean log;

        public Config() {
        }

        public Config(boolean log) {
            this.log = log;
        }

        public boolean getLog() {
            return log;
        }

        public void setLog(boolean log) {
            this.log = log;
        }
    }
}

在这里插入图片描述

1.5.2.2.配置过滤器
spring:
  cloud:
      routes:
        - id: sentinel-consumer
          uri: lb://sentinel-consumer
          predicates:
            - Path=/consumer/**
          filters: #自定义过滤器
            - Log=true
1.5.2.3.测试
  1. 浏览器访问:http://127.0.0.1:9527/consumer/getUserById/1

在这里插入图片描述

1.5.3.自定义全局过滤器

  • 需求:

在网关过滤器中通过Token 判断用户是否登录

  • 全局过滤器(GlobalFilter):

作用于所有路由,Spring Cloud Gateway 定义了Global Filter接口,用户可以自定义实现自己的Global Filter。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能,并且全局过滤器也是程序员使用比较多的过滤器。Spring Cloud Gateway内部也是通过一系列的内置全局过滤器对整个路由转发进行处理如下:
在这里插入图片描述

1.5.2.1.创建过滤器

在服务网关中定义过滤器只需要实现 GlobalFilter, Ordered接口就可对请求进行拦截与过滤。

package com.bjpowernode.FilterFactory;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;

@Component
public class LoginFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        String token = request.getQueryParams().getFirst("token");
        if (token == null) {
            BaseResult data = new BaseResult(401, "未登录");
            return response(response,data);
        }
        //放行
        return chain.filter(exchange);
    }

    private Mono<Void> response(ServerHttpResponse response, BaseResult data) {
        String jsonData = null;
        try {
            jsonData = new ObjectMapper().writeValueAsString(data);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        DataBuffer buffer = 
            response.bufferFactory().wrap(jsonData.getBytes(StandardCharsets.UTF_8));
        response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        return response.writeWith(Mono.just(buffer));
    }

    /**
     * 过滤器的执行顺序:通过整数表示顺序,数值越小,优先级越高
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }
}
class BaseResult {
    private int status;
    private String msg;
    private Object data;

    public BaseResult() {
    }

    public BaseResult(int status, String msg) {
        this.status = status;
        this.msg = msg;
    }

    public BaseResult(int status, String msg, Object data) {
        this.status = status;
        this.msg = msg;
        this.data = data;
    }

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
}
1.5.2.2.测试
  1. 不加token:http://127.0.0.1:9527/gateway/consumer/getUserById?id=1&nam=zs
    在这里插入图片描述

  2. 加token:http://127.0.0.1:9527/consumer/getUserById/1?token=123

在这里插入图片描述

1.5.限流

网关是所有请求的公共入口,所以可以在网关进行限流,而且限流的方式也很多,我们本次采用前 面学过的Sentinel组件来实现网关的限流。

1.5.1.api_gateway

1.5.1.1.pom.xml
        <!--sentinel的启动器-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <!--spring cloud gateway整合sentinel的启动器-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
        </dependency>
1.5.1.2.application.yml
spring:
  cloud:
    sentinel:
      transport:
        dashboard: 127.0.0.1:8080 #指定sentinel的地址
1.5.1.3.自定义异常处理
package com.bjpowernode.exception;

import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import com.alibaba.csp.sentinel.slots.system.SystemBlockException;
import com.alibaba.fastjson.JSON;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class GatewayConfig {
 
    @PostConstruct
    public void init() {
        BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
            @Override
            public Mono<ServerResponse> handleRequest(ServerWebExchange exchange,
                                                      				Throwable t) {
                // 自定义异常信息
                Map map = new HashMap<>();
                map.put("status", 200);
                map.put("msg", "接口被限流了");
                return ServerResponse.status(HttpStatus.OK)
                        .contentType(MediaType.APPLICATION_JSON)
                        .body(BodyInserters.fromValue(map));
            }
        };
        //自定义异常处理
        GatewayCallbackManager.setBlockHandler(blockRequestHandler);
    }
}

1.5.2.测试

  • 先启动sentinel的持久化到nacos的控制台
  • http://127.0.0.1:8080 账号和密码都是sentinel
  1. 新增流控规则
    在这里插入图片描述
  • 5秒内访问到第3次
    在这里插入图片描述
  1. 高并发访问:http://127.0.0.1:9527/consumer/getUserById/1

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值