目录
使用Postman测试Spring Cloud Gateway的限流器有以下步骤:
一、网关简介
(一)为什么要用网关
大家都都知道在微服务架构中,一个系统会被拆分为很多个微服务。那么作为客户端要如何去调用 这么多的微服务呢?如果没有网关的存在,我们只能在客户端记录每个微服务的地址,然后分别去调用。
左图这样的架构,会存在着诸多的问题:
-
客户端多次请求不同的微服务,增加客户端代码或配置编写的复杂性
-
认证复杂,每个服务都需要独立认证。
-
存在跨域请求,在一定场景下处理相对复杂。
(二)网关解决了什么问题
(三)常用的网关
在业界比较流行的网关,有下面这些:
-
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简介
(一)核心概念
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维度的限流已经配置成功,小伙伴可以自己照着上述步骤尝试一下。