目录
一、网关简介
(一)为什么要用网关
(二)网关解决了什么问题
(三)常用的网关
二、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异步调用解决。
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维度的限流已经配置成功,小伙伴可以自己照着上述步骤尝试一下。