SpringCloudAlibaba:服务网关之Gateway学习

目录

一、网关简介

(一)为什么要用网关

(二)网关解决了什么问题

(三)常用的网关

二、Gateway简介

(一)核心概念

(二)工作原理

三、Gateway快速入门

(一) 基础版

(二) 增强版

(三) 简写版

四、断言

问题总结:

内置路由断言工厂

五、过滤器

(一)网关过滤器(GatewayFilter)

(二)全局过滤器(GlobalFilter)

1、自定义全局过滤器-token验证

2、自定义全局过滤器-鉴权

六、网关限流

(一)接口限流

使用Postman测试Spring Cloud Gateway的限流器有以下步骤:

(二)Gateway整合Sentinel实现网关限流

1.网关如何限流?

2.实战演示


一、网关简介

(一)为什么要用网关

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

左图这样的架构,会存在着诸多的问题:

  • 客户端多次请求不同的微服务,增加客户端代码或配置编写的复杂性

  • 认证复杂,每个服务都需要独立认证。

  • 存在跨域请求,在一定场景下处理相对复杂。

(二)网关解决了什么问题

(三)常用的网关

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

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

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

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

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

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

二、Gateway简介

Gateway官网

(一)核心概念

 id,路由标识符,区别于其他 Route。

uri,路由指向的目的地 uri,即客户端请求最终被转发到的微服务。

order,用于多个 Route 之间的排序,数值越小排序越靠前,匹配优先级越高。

predicate,断言的作用是进行条件判断,只有断言都返回真,才会真正的执行路由。

filter,过滤器用于修改请求和响应信息。

(二)工作原理

执行流程大体如下:

  • Gateway Client向Gateway Server发送请求

  • 请求首先会被HttpWebHandlerAdapter进行提取组装成网关上下文

  • 然后网关的上下文会传递到DispatcherHandler,它负责将请求分发给 RoutePredicateHandlerMapping

  • RoutePredicateHandlerMapping负责路由查找,并根据路由断言判断路由是否可用

  • 如果过断言成功,由FilteringWebHandler创建过滤器链并调用

  • 请求会一次经过PreFilter–微服务–PostFilter的方法,最终返回响应

三、Gateway快速入门

(一) 基础版

第1步:创建一个 api-gateway 的模块,导入相关依赖

    <dependencies>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

第2步: 创建主类

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

第3步: 添加配置文件

spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:  # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
        - id: user_server  # 当前路由的标识, 要求唯一
          uri: http://127.0.0.1:8006  # 请求要转发到的地址
          order: 1  # 路由的优先级,数字越小级别越高
          predicates:  # 断言(就是路由转发要满足的条件)
            - Path=/user-server/**  # 当请求路径满足Path指定的规则时,才进行路由转发
          filters:  # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
            - StripPrefix=1  # 转发之前去掉1层路径

第4步: 启动项目, 并通过网关去访问微服务

http://127.0.0.1:7000/user-server/user/get1    http://127.0.0.1:8006/user/get1

以上两个请求的效果是相同的

(二) 增强版

现在在配置文件中写死了转发路径的地址, 前面我们已经分析过地址写死带来的问题, 接下来我们从注册中心获取此地址。

第1步:加入nacos依赖

<!--nacos客户端依赖--> 基础版的以来中已经有了

第2步:在主类上添加注解

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

第3步:修改配置文件

spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    gateway:
      discovery:
        locator:
          enabled: true # 让gateway可以发现nacos中的微服务
      routes:
        - id: user_server
          uri: lb://user-server   # lb指的是从nacos中按照名称获取微服务,并遵循负
          order: 1
          predicates:
            - Path=/user-server/**
          filters:
            - StripPrefix=1
server:
  port: 7000

第4步:测试 http://127.0.0.1:7000/user-server/user/get1

(三) 简写版

第1步: 去掉关于路由的配置

spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    gateway:
      discovery:
        locator:
          enabled: true # 让gateway可以发现nacos中的微服务
server:
  port: 7000

第2步: 启动项目,并通过网关去访问微服务 http://127.0.0.1:7000/user-server/user/get1 这时候,就发现只要按照网关地址/微服务/接口的格式去访问,就可以得到成功响应

四、断言

Predicate(断言, 谓词) 用于进行条件判断,只有断言都返回真,才会真正的执行路由。 断言就是说: 在 什么条件下 才能进行路由转发

问题总结:

1.在Spring Cloud Gateway中,如果我们启用了service discovery(配置了gateway.discovery.locator.enabled=true),那么对于没有配置路由映射的微服务,其请求是否可以正常访问?

答案是:可以正常访问。启用service discovery后,Spring Cloud Gateway会自动根据服务名创建路由配置。

2.在Spring Cloud Gateway的路由规则中,Path断言中的路径值user-server是否可以随意填写?

答案是:不能随意填写,它必须与实际的服务地址或路由相对应。以路由规则为例:

      routes:
        - id: user_server
          uri: lb://user-server  
          order: 1
          predicates:  
            - Path=/user-server/**

这里的Path=/user-server/** 表示,任何以/user-server/开头的请求路径都会被该路由规则匹配并转发。但这里的user-server必须与uri的值 lb://xn--user-server-9n3uz6x 。lb表示从服务注册中心获取实际服务地址。所以,如果Path的值写成/other-server/**,则该路由规则的Path断言将永远不会匹配任何请求,造成死路由。

正确的访问路径应该是:/user-server/some/path如果写成:/other-server/some/path则该请求无法找到任何匹配的路由进行转发,会执行fallback配置的逻辑。

所以,Path断言中的路径不可以随意填写,它必须与实际的后端服务地址或路由URI对应,否则会产生如下问题: 死路由、路径混淆、无法访问。


内置路由断言工厂

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

  1. 基于Datetime类型的断言工厂

    此类型的断言根据时间做判断,主要有三个: AfterRoutePredicateFactory: 接收一个日期参数,判断请求日期是否晚于指定日期 BeforeRoutePredicateFactory: 接收一个日期参数,判断请求日期是否早于指定日期 BetweenRoutePredicateFactory: 接收两个日期参数,判断请求日期是否在指定时间段内

    spring:
      application:
        name: gateway
      cloud:
        nacos:
          discovery:
            server-addr: 127.0.0.1:8848
        gateway:
          discovery:
            locator:
              enabled: true
          routes:
          - id: after_route
            uri: https://example.org
            predicates:
                - After=2017-01-20T17:42:47.789-07:00[America/Denver] # 时间点后匹配
                - Before=2017-01-20T17:42:47.789-07:00[America/Denver] # 时间点前匹配
                - Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver] # 时间区间匹配
          - id:    
  2. 基于远程地址的断言工厂

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

            predicates:
                - RemoteAddr=192.168.1.1/24
  3. 基于Cookie的断言工厂

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

            predicates:
                - Cookie=chocolate, ch.p
  4. 基于Header的断言工厂 HeaderRoutePredicateFactory:接收两个参数,标题名称和正则表达式。判断请求Header是否 具有给定名称且值与正则表达式匹配。

            predicates:
                - Header=X-Request-Id, \d+
  5. 基于Host的断言工厂 HostRoutePredicateFactory:接收一个参数,主机名模式。判断请求的Host是否满足匹配规则。

            predicates:
                - Host=**.somehost.org,**.anotherhost.org
  6. 基于Method请求方法的断言工厂 MethodRoutePredicateFactory:接收一个参数,判断请求类型是否跟指定的类型匹配。

            predicates:
                - Method=GET,POST
  7. 基于Path请求路径的断言工厂 PathRoutePredicateFactory:接收一个参数,判断请求的URI部分是否满足路径规则。

            predicates:
                - Path=/red/{segment},/blue/{segment}
  8. 基于Query请求参数的断言工厂 QueryRoutePredicateFactory :接收两个参数,请求param和正则表达式, 判断请求参数是否具 有给定名称且值与正则表达式匹配。

            predicates:
                - Query=green
  9. 基于路由权重的断言工厂 WeightRoutePredicateFactory:接收一个[组名,权重],然后对于同一个组内的路由按照权重转发

    该路由会将约90%的流量转发至Transforming Lives, One Pound At A Time | Weight High并将约10%的流量转发至最佳欧洲杯赔率表,投注必赢策略分享 - 2024年欧洲足球锦标赛

    routes:
       -id: weight_route1 
       uri: https://weighthigh.org
       predicates:
           -Path=/product/**
           -Weight=group3, 1
       -id: weight_route2 
       uri: https://weighlow.org 
       predicates:
           -Path=/product/**
           -Weight= group3, 9

五、过滤器

三个知识点:

  1. 作用: 过滤器就是在请求的传递过程中,对请求和响应做一些手脚

  2. 生命周期: Pre Post

  3. 分类: 局部过滤器(作用在某一个路由上) 全局过滤器(作用全部路由上)

在Gateway中, Filter的生命周期只有两个:“pre” 和 “post”。

  1. PRE: 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。

  2. POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTPHeader、收集统计信息和指标、将响应从微服务发送给客户端等。

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

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

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

(一)网关过滤器(GatewayFilter)

局部过滤器:

spring:  
  cloud:  
    gateway:
      discovery:
        locator:
          enabled: true # 让gateway可以发现nacos中的微服务
      routes:  # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
        - id: user_server  # 当前路由的标识, 要求唯一
          uri: lb://user-server  # 请求要转发到的地址
          order: 1  # 路由的优先级,数字越小级别越高
          predicates:  # 断言(就是路由转发要满足的条件)
            - Path=/user-server/**  # 当请求路径满足Path指定的规则时,才进行路由转发
          filters:  # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
            - StripPrefix=1  # 转发之前去掉1层路径

(二)全局过滤器(GlobalFilter)

全局过滤器作用于所有路由, 无需配置。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能。

1、自定义全局过滤器-token验证

内置的过滤器已经可以完成大部分的功能,但是对于企业开发的一些业务功能处理,还是需要我们自己编写过滤器来实现的,那么我们一起通过代码的形式自定义一个过滤器,去完成统一的权限校验。 开发中的鉴权逻辑:

  1. 当客户端第一次请求服务时,服务端对用户进行信息认证(登录)

  2. 认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证

  3. 以后每次请求,客户端都携带认证的token

  4. 服务端对token进行解密,判断是否有效。

如上图,对于验证用户是否已经登录鉴权的过程可以在网关统一检验。 检验的标准就是请求中是否携带token凭证以及token的正确性。 下面的我们自定义一个GlobalFilter,去校验所有请求的请求参数中是否包含“token”,如何不包含请求参数“token”则不转发路由,否则执行正常的逻辑。(将token存到radis缓存)

1.导redis的jar包

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2.redis配置类

@Configuration
public class RedisConfig {
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;
​
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate= new RedisTemplate<>();
        // 可根据需要添加序列化器
        // template.setDefaultSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
        // key采用String的序列化方式
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }
}

3.redis工具类

/**
 * @Description: com.buba.utils   Redis工具类
 */
@Component
public final class RedisUtil {
​
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    // =============================common============================
    /**
     * 指定缓存失效时间
     * @param key 键
     * @param time 时间(秒)
     * @return
     */
    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 根据key 获取过期时间
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }
    /**
     * 判断key是否存在
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 删除缓存
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));
            }
        }
    }
    // ============================String=============================
    /**
     * 普通缓存获取
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }
    /**
     * 普通缓存放入
     * @param key 键
     * @param value 值
     * @return true成功 false失败
     */
    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 普通缓存放入并设置时间
     * @param key 键
     * @param value 值
     * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */
    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 递增
     * @param key 键
     * @param delta 要增加几(大于0)
     * @return
     */
    public long incr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }
    /**
     * 递减
     * @param key 键
     * @param delta 要减少几(小于0)
     * @return
     */
    public long decr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }
    // ================================Map=================================
    /**
     * HashGet
     * @param key 键 不能为null
     * @param item 项 不能为null
     * @return 值
     */
    public Object hget(String key, String item) {
        return redisTemplate.opsForHash().get(key, item);
    }
    /**
     * 获取hashKey对应的所有键值
     * @param key 键
     * @return 对应的多个键值
     */
    public Map<Object, Object> hmget(String key) {
        return redisTemplate.opsForHash().entries(key);
    }
    /**
     * HashSet
     * @param key 键
     * @param map 对应多个键值
     * @return true 成功 false 失败
     */
    public boolean hmset(String key, Map<String, Object> map) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * HashSet 并设置时间
     * @param key 键
     * @param map 对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */
    public boolean hmset(String key, Map<String, Object> map, long time) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 向一张hash表中放入数据,如果不存在将创建
     * @param key 键
     * @param item 项
     * @param value 值
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 向一张hash表中放入数据,如果不存在将创建
     * @param key 键
     * @param item 项
     * @param value 值
     * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value, long time) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 删除hash表中的值
     * @param key 键 不能为null
     * @param item 项 可以使多个 不能为null
     */
    public void hdel(String key, Object... item) {
        redisTemplate.opsForHash().delete(key, item);
    }
    /**
     * 判断hash表中是否有该项的值
     * @param key 键 不能为null
     * @param item 项 不能为null
     * @return true 存在 false不存在
     */
    public boolean hHasKey(String key, String item) {
        return redisTemplate.opsForHash().hasKey(key, item);
    }
    /**
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     * @param key 键
     * @param item 项
     * @param by 要增加几(大于0)
     * @return
     */
    public double hincr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, by);
    }
    /**
     * hash递减
     * @param key 键
     * @param item 项
     * @param by 要减少记(小于0)
     * @return
     */
    public double hdecr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, -by);
    }
    // ============================set=============================
    /**
     * 根据key获取Set中的所有值
     * @param key 键
     * @return
     */
    public Set<Object> sGet(String key) {
        try {
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    /**
     * 根据value从一个set中查询,是否存在
     * @param key 键
     * @param value 值
     * @return true 存在 false不存在
     */
    public boolean sHasKey(String key, Object value) {
        try {
            return redisTemplate.opsForSet().isMember(key, value);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 将数据放入set缓存
     * @param key 键
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSet(String key, Object... values) {
        try {
            return redisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    /**
     * 将set数据放入缓存
     * @param key 键
     * @param time 时间(秒)
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSetAndTime(String key, long time, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().add(key, values);
            if (time > 0)
                expire(key, time);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    /**
     * 获取set缓存的长度
     * @param key 键
     * @return
     */
    public long sGetSetSize(String key) {
        try {
            return redisTemplate.opsForSet().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    /**
     * 移除值为value的
     * @param key 键
     * @param values 值 可以是多个
     * @return 移除的个数
     */
    public long setRemove(String key, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().remove(key, values);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    // ===============================list=================================
    /**
     * 获取list缓存的内容
     * @param key 键
     * @param start 开始
     * @param end 结束 0 到 -1代表所有值
     * @return
     */
    public List<Object> lGet(String key, long start, long end) {
        try {
            return redisTemplate.opsForList().range(key, start, end);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    /**
     * 获取list缓存的长度
     * @param key 键
     * @return
     */
    public long lGetListSize(String key) {
        try {
            return redisTemplate.opsForList().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    /**
     * 通过索引 获取list中的值
     * @param key 键
     * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
     * @return
     */
    public Object lGetIndex(String key, long index) {
        try {
            return redisTemplate.opsForList().index(key, index);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, Object value) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @param time 时间(秒)
     * @return
     */
    public boolean lSet(String key, Object value, long time) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, List<Object> value) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 将list放入缓存
     *
     * @param key 键
     * @param value 值
     * @param time 时间(秒)
     * @return
     */
    public boolean lSet(String key, List<Object> value, long time) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 根据索引修改list中的某条数据
     * @param key 键
     * @param index 索引
     * @param value 值
     * @return
     */
    public boolean lUpdateIndex(String key, long index, Object value) {
        try {
            redisTemplate.opsForList().set(key, index, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 移除N个值为value
     * @param key 键
     * @param count 移除多少个
     * @param value 值
     * @return 移除的个数
     */
    public long lRemove(String key, long count, Object value) {
        try {
            Long remove = redisTemplate.opsForList().remove(key, count, value);
            return remove;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
​
}

4.yml配置文件

server:
  port: 7002
spring:
  redis: # window
    port: 6379
    host: 127.0.0.1
  application:
    name: gateway-server
  cloud:
    nacos: # Linux
      discovery:
        username: nacos
        password: nacos
        server-addr: 192.168.177.129:8848
    gateway:
      discovery:
        locator:
          enabled: true

5.Token过滤器

@Component
public class TokenFilter implements GlobalFilter, Ordered {
​
    @Autowired
    private RedisUtil redisUtil;
​
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //1、获取请求对象  ServerHttpRequest
        ServerHttpRequest request = exchange.getRequest();
        //2、获取请求的资源路径
        String path = request.getURI().getPath();
​
        //3、判断当前路径是否是不需要登录的资源路径—不需要则放过请求直接去执行目标接口
        //                                  —需要登录则如下判断token
        if(!"/user-server/user/login".equals(path) && !("/user-server/user/regist".equals(path))){
            //4、获取到请求头中的token
            List<String> tokens = request.getHeaders().get("token");
            String token = (tokens!=null&&tokens.size()>0)?tokens.get(0):null;
            //5、获取到请求头中的uid
            List<String> uids = request.getHeaders().get("uid");
            String uid = (uids!=null&&uids.size()>0)?uids.get(0):null;
            if(token!=null && uid!=null){
                //6、获取redis中的token
                String redis_token = String.valueOf(redisUtil.get("TOKEN_"+uid));
                //7、验证token是否有效
                if(redis_token==null|| "".equals(redis_token) || !redis_token.equals(token)){
                    //8、无效则返回错误相应
                    return onFailure(exchange.getResponse(),"token失效Q");
                }
​
            }else{
                //6、没有携带token返回错误相应
                return onFailure(exchange.getResponse(),"未登录,请先登录!");
            }
        }
​
        //去找执行目标方法
        return chain.filter(exchange);
    }
​
    @Override
    public int getOrder() {
        return 0;
    }
​
    public Mono<Void> onFailure(ServerHttpResponse response, String mes){
        JsonObject message = new JsonObject();
        message.addProperty("success", false);
        message.addProperty("code", 403);
        message.addProperty("data", mes);
        byte[] bits = message.toString().getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = response.bufferFactory().wrap(bits);
        //response.setStatusCode(HttpStatus.UNAUTHORIZED);
        //指定编码,否则在浏览器中会中文乱码
        response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        return response.writeWith(Mono.just(buffer));
    }
}

6.测试

按正常来说登录的时候要在redis中设置redisUtil.set("TOKEN_"+uid,"用户id"),现在模拟在redis中set一个请求头信息 给111用户设置一个123456的token。

2、自定义全局过滤器-鉴权

1.在网关中判断当前用户是否有权限

import com.alibaba.nacos.shaded.com.google.gson.JsonObject;
import com.buba.feign.UserFeign;
import com.buba.utils.RedisUtil;
import feign.Feign;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Lazy;
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.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
​
import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.CompletableFuture;
​
@Component
public class TokenFilter
        implements GlobalFilter, Ordered
        {
​
    @Autowired
    private RedisUtil redisUtil;
​
    @Lazy // 注意:注入使用懒加载,在gateway网关中不能使用openfeign同步调用,需要采取异步方式
    @Resource
    private UserFeign userFeign;
​
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //1、获取请求对象  ServerHttpRequest
        ServerHttpRequest request = exchange.getRequest();
        //2、获取请求的资源路径
        String path = request.getURI().getPath();
        //5、获取到请求头中的uid
        List<String> uids = request.getHeaders().get("uid");
        String uid = (uids!=null&&uids.size()>0)?uids.get(0):null;
​
        //3、判断当前路径是否是不需要登录的资源路径—不需要则放过请求直接去执行目标接口
        //                                  —需要登录则如下判断token
        if(!"/user-server/user/login".equals(path) && !("/user-server/user/regist".equals(path))){
            //4、获取到请求头中的token
            List<String> tokens = request.getHeaders().get("token");
            String token = (tokens!=null&&tokens.size()>0)?tokens.get(0):null;
​
            if(token!=null && uid!=null){
                //6、获取redis中的token
                String redis_token = String.valueOf(redisUtil.get("TOKEN_"+uid));
                //7、验证token是否有效
                if(redis_token==null|| "".equals(redis_token) || !redis_token.equals(token)){
                    //8、无效则返回错误相应
                    return onFailure(exchange.getResponse(),"token失效Q");
                }
​
            }else{
                //6、没有携带token返回错误相应
                return onFailure(exchange.getResponse(),"未登录,请先登录!");
            }
        }
        //以下为新增内容根据用户id查询该用户拥有的资源接//
        if(uid!=null && !"".equals(uid) ){
            Long aLong = Long.valueOf(uid) ;
            // 注意:注入使用懒加载,在gateway网关中不能使用openfeign同步调用,需要采取异步方式
             // 异步调用feign服务接口
            CompletableFuture<List<String>> com =
                    CompletableFuture.supplyAsync(()->{
                        return userFeign.selectResByUid(aLong);
                    });
            List<String> u = null;
            try {
                u = com.get();
            }catch (Exception ex){
                ex.printStackTrace();
            }
            boolean b = u.contains(path);
            if(!b){
                return onFailure(exchange.getResponse(),"您没有该资源的访问权限!");
            }
        }
//        //去找执行目标方法
        return chain.filter(exchange);
    }
    @Override
    public int getOrder() {
        return 0;
    }
    public Mono<Void> onFailure(ServerHttpResponse response, String mes){
        JsonObject message = new JsonObject();
        message.addProperty("success", false);
        message.addProperty("code", 403);
        message.addProperty("data", mes);
        byte[] bits = message.toString().getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = response.bufferFactory().wrap(bits);
        //response.setStatusCode(HttpStatus.UNAUTHORIZED);
        //指定编码,否则在浏览器中会中文乱码
        response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        return response.writeWith(Mono.just(buffer));
    }
}

上面代码中,网关过滤器通过Feign调用user服务中的方法(selectResByUid),去查找数据库中用户的访问资源,如果存在就放过,如果没有就返回。

2.使用Feign调用

1)准备Feign接口

@FeignClient(name = "user-server", path = "/user")
public interface UserFeign {
    @GetMapping("/getPaths")
    List<String> selectResByUid(@RequestParam("uid") Long uid);
}

2)启动类上加注解:@EnableFeignClients

3)user模块Controller层准备对应的方法,该控制器方法的存在,就是为了让网关过滤器调用

// 测试获取用户访问权限
@GetMapping("/getPaths")
public List<String> getPaths(@RequestParam("uid") Long uid){
    List<String> strings = userMapper.selectResByUid(uid);
    return strings;
}

3.这之后出现错误,解决办法

1)服务器启动转圈

Gateway服务器启动不成功,一直转圈,也不报错。在网关过滤器上自动装配Feign接口上加注解:@Lazy

2)报阻塞异常

在过滤类中正常调用feign服务接口时,会抛出一个java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-2,意思是线程堵塞,使用CompletableFuture.supplyAsync异步调用解决。

 哪里使用Feign接口调用其他服务的控制器方法,那么就在哪里使用CompletableFuture.supplyAsync异步调用解决。

3)报空指针异常

不管怎么访问资源路径,Debug模式查看过滤器中Feign调用的方法获取到的结果一直为空:

加上Feign配置类,解决异步调用 feign 的错误。

import feign.Logger;
import feign.codec.Decoder;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.support.ResponseEntityDecoder;
import org.springframework.cloud.openfeign.support.SpringDecoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import java.util.ArrayList;
import java.util.List;
​
@Configuration
public class FeignConfig {
    @Bean
    Logger.Level feignLevel() {
        //这里记录所有
        return Logger.Level.FULL;
    }
​
    @Bean
    public Decoder feignDecoder() {
        return new ResponseEntityDecoder(new SpringDecoder(feignHttpMessageConverter()));
    }
​
    public ObjectFactory<HttpMessageConverters> feignHttpMessageConverter() {
        final HttpMessageConverters httpMessageConverters = new HttpMessageConverters(new PhpMappingJackson2HttpMessageConverter());
        return new ObjectFactory<HttpMessageConverters>() {
            @Override
            public HttpMessageConverters getObject() throws BeansException {
                return httpMessageConverters;
            }
        };
    }
​
    public class PhpMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {
        PhpMappingJackson2HttpMessageConverter(){
            List<MediaType> mediaTypes = new ArrayList<>();
            mediaTypes.add(MediaType.valueOf(MediaType.TEXT_HTML_VALUE + ";charset=UTF-8"));
            setSupportedMediaTypes(mediaTypes);
        }
    }
}

4.成功了

Navicat:表

Postman

访问数据库中存在的资源,则返回结果

如果访问的资源没有权限,则返回

六、网关限流

(一)接口限流

GateWay限流是基于Redis使用令牌桶算法实现的,所以要引入Redis依赖,以及配置Redis参数信息

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

使用Postman测试Spring Cloud Gateway的限流器有以下步骤:

  1. 启动Gateway与Redis服务器,确保限流器配置正确并生效。

  2. 找到限流器所在的路由地址与配置。例如: 配置限流的过滤器信息

server:
  port: 7002
spring:
  redis:
    port: 6379
    host: 127.0.0.1
  application:
    name: gateway-server
  cloud:
    nacos:
      discovery:
        username: nacos
        password: nacos
        server-addr: 192.168.177.129:8848
    gateway:
      routes:
        - id: user-server
          uri: lb://user-server
          predicates:
            - Path=/user-server/**
          filters:
            - StripPrefix=1
            - name: RequestRateLimiter
              args:
                key-resolver: '#{@ipKeyResolver}'
                # 指定了令牌桶每秒填充速率,表示每秒钟可以放入的请求数量
                redis-rate-limiter.replenishRate: 1  
                # 指定了令牌桶的容量,即最大允许的瞬时并发请求数量。
                redis-rate-limiter.burstCapacity: 2   

该路由的地址为:/user-server/**,

filter 名称必须是 RequestRateLimiter。

限流参数为:

  • key-resolver:使用 SpEL 按名称引用 bean。 用于指定限流时使用的键解析器(Key Resolver)

@Configuration
public class AppCoonfig {
    @Bean
    public KeyResolver ipKeyResolver(){
​
        return new KeyResolver() {
            @Override
            public Mono<String> resolve(ServerWebExchange exchange) {
                return  Mono.just(exchange.getRequest().getPath().value());
            }
        };
    }
}
  • replenishRate: 1,每秒新增1个令牌

  • burstCapacity: 2,令牌桶最大容量2个令牌

  1. 点击"Send"按钮,观察响应结果。

    如果返回200状态码,表示此时令牌桶中还有令牌,请求被放行。

    如果返回429状态码,表示令牌桶中令牌不足,请求被限流。

  2. 在1秒内反复点击"Send"按钮,当触发限流时会出现429响应。

  3. 此时停止点击1秒,等待令牌桶填充新的1个令牌。(也就是等待2秒,桶就填充满了)

  4. 再次点击"Send"按钮,会再次得到200响应,表示新的令牌已填充。


(二)Gateway整合Sentinel实现网关限流

1.网关如何限流?

从1.6.0版本开始,Sentinel提供了SpringCloud Gateway的适配模块,可以提供两种资源维度的限流:

  • route维度:即在配置文件中配置的路由条目,资源名为对应的routeId,这种属于粗粒度的限流,一般是对某个微服务进行限流。

  • 自定义API维度:用户可以利用Sentinel提供的API来自定义一些API分组,这种属于细粒度的限流,针对某一类的uri进行匹配限流,可以跨多个微服务。

Spring Cloud Gateway集成Sentinel实现很简单,这就是阿里的魅力,提供简单、易操作的工具,让程序员专注于业务。

2.实战演示

1)gateway模块,添加如下依赖:

<!--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>
<!--sentinelH整合gateway的依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
<!--sentinel的依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

注意:这依然是一个网关服务,不要添加WEB的依赖

2)yml配置文件

配置文件中主要指定以下三种配置:

  • nacos的地址

  • sentinel控制台的地址

  • 网关路由的配置

server:
  port: 7002
spring:
  application:
    name: gateway-server
  redis:
    port: 6379
    host: 127.0.0.1
  cloud:
    # 开始gateway的配置
    gateway:
      routes:
        - id: user_server
          uri: lb://user-server
          predicates:
            - Path=/user-server/**
          filters:
            - StripPrefix=1
    # nacos的配置
    nacos:
      discovery:
        server-addr: 192.168.177.129:8848  # nacos注册地址
        username: nacos
        password: nacos
    # sentinel配置
    sentinel:
      transport:
        # 指定sentinel控制台远程地址
        dashboard: 192.168.177.129:8081
        port: 8719
      # 直接建立心跳
      eager: true
      scg:
        # 限流后的响应配置
        fallback:
          content-type: application/json
          # 模式 response、redirect
          mode: response
          # 响应状态码
          response-status: 429
          # 响应信息
          response-body: 对不起,已经被限流了!!!

上述配置中设置了一个路由user_server,只要请求路径满足/user_server/**都会被路由到user_server这个服务中。

3)限流配置

经过上述两个步骤其实已经整合好了Sentinel,此时访问sentinel控制台,

然后在sentinel控制台可以看到已经被监控了,监控的路由是user_server,如下图:

 此时我们可以为其新增一个route维度的限流,如下图:

上图中对user-server这个路由做出了限流,QPS阈值为1。

此时快速访问:http://localhost:7002/user-server/user/get6,看到已经被限流了,如下图:

以上route维度的限流已经配置成功,小伙伴可以自己照着上述步骤尝试一下。  

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值