目录
一、网关简介
(一)为什么要用网关
(二)网关解决了什么问题
(三)常用的网关
二、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请求的不同属性匹配。具体如下:
-  基于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: 
-  基于远程地址的断言工厂 RemoteAddrRoutePredicateFactory:接收一个IP地址段,判断请求主机地址是否在地址段中 predicates: - RemoteAddr=192.168.1.1/24 
-  基于Cookie的断言工厂 CookieRoutePredicateFactory:接收两个参数,cookie 名字和一个正则表达式。 判断请求 cookie是否具有给定名称且值与正则表达式匹配。 predicates: - Cookie=chocolate, ch.p 
-  基于Header的断言工厂 HeaderRoutePredicateFactory:接收两个参数,标题名称和正则表达式。判断请求Header是否 具有给定名称且值与正则表达式匹配。 predicates: - Header=X-Request-Id, \d+ 
-  基于Host的断言工厂 HostRoutePredicateFactory:接收一个参数,主机名模式。判断请求的Host是否满足匹配规则。 predicates: - Host=**.somehost.org,**.anotherhost.org 
-  基于Method请求方法的断言工厂 MethodRoutePredicateFactory:接收一个参数,判断请求类型是否跟指定的类型匹配。 predicates: - Method=GET,POST 
-  基于Path请求路径的断言工厂 PathRoutePredicateFactory:接收一个参数,判断请求的URI部分是否满足路径规则。 predicates: - Path=/red/{segment},/blue/{segment}
-  基于Query请求参数的断言工厂 QueryRoutePredicateFactory :接收两个参数,请求param和正则表达式, 判断请求参数是否具 有给定名称且值与正则表达式匹配。 predicates: - Query=green 
-  基于路由权重的断言工厂 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
五、过滤器
三个知识点:
-  作用: 过滤器就是在请求的传递过程中,对请求和响应做一些手脚 
-  生命周期: Pre Post 
-  分类: 局部过滤器(作用在某一个路由上) 全局过滤器(作用全部路由上) 
在Gateway中, Filter的生命周期只有两个:“pre” 和 “post”。
-  PRE: 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。 
-  POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTPHeader、收集统计信息和指标、将响应从微服务发送给客户端等。 
Gateway 的Filter从作用范围可分为两种: GatewayFilter与GlobalFilter。
-  GatewayFilter:应用到单个路由或者一个分组的路由上。 
-  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验证
内置的过滤器已经可以完成大部分的功能,但是对于企业开发的一些业务功能处理,还是需要我们自己编写过滤器来实现的,那么我们一起通过代码的形式自定义一个过滤器,去完成统一的权限校验。 开发中的鉴权逻辑:
-  当客户端第一次请求服务时,服务端对用户进行信息认证(登录) 
-  认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证 
-  以后每次请求,客户端都携带认证的token 
-  服务端对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异步调用解决。
 哪里使用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的限流器有以下步骤:
-  启动Gateway与Redis服务器,确保限流器配置正确并生效。 
-  找到限流器所在的路由地址与配置。例如: 配置限流的过滤器信息 
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个令牌 
-  点击"Send"按钮,观察响应结果。 如果返回200状态码,表示此时令牌桶中还有令牌,请求被放行。 如果返回429状态码,表示令牌桶中令牌不足,请求被限流。 
-  在1秒内反复点击"Send"按钮,当触发限流时会出现429响应。 
-  此时停止点击1秒,等待令牌桶填充新的1个令牌。(也就是等待2秒,桶就填充满了) 
-  再次点击"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维度的限流已经配置成功,小伙伴可以自己照着上述步骤尝试一下。



















