spring cloud ——gateway网关
什么是网关?
网关是一个服务器,是系统的唯一入口,封装了系统内部架构,为客户端提供统一的API接口,比如gateway网关,它是微服务架构中的核心组件,作为系统的统一入口,负责请求路由、负载均衡、安全控制等功能。
主要功能
- 请求路由:将外部请求转发到相应的后端服务
- 负载均衡:在多个服务实例间分配请求
- 身份认证:统一处理用户认证和授权
- 限流熔断:保护后端服务不被流量冲击
- 协议转换:处理不同协议间的转换
- 日志监控:记录请求日志,便于监控和分析
Gateway核心概念
- 路由(route)
路由信息的组成:由一个ID、一个目的URL、一组断言工厂、一组Filter组成。如果路由断言为真,说明请求URL和配置路由匹配。
- 断言(Predicate)
Spring Cloud Gateway中的断言函数输入类型是Spring, 5.0框架中的ServerWebExchange。Spring Cloud Gateway的断言函数允许开发者去定义匹配来自于HTTP Request中的任何信息比如请求头和参数。
- 过滤器(Filter)
一个标准的Spring WebFilter。 Spring Cloud Gateway中的Filter分为两种类型的Filter,分别是GatewayFilter和GlobalFilter。过滤器Filter将会对请求和响应进行修改处理
工作原理:
常见网关实现
- Spring Cloud Gateway:Spring Cloud官方推荐的网关
- Zuul:Netflix开源的网关组件
- Nginx:高性能的反向代理服务器
- Kong:基于OpenResty的API网关
- API Gateway:云服务商提供的网关服务
优势
- 统一入口,简化客户端调用
- 集中处理横切关注点(安全、监控等)
- 提高系统安全性和可维护性
- 支持灰度发布和A/B测试
- 网关是微服务架构中不可或缺的组件,提供了统一的API管理和流量控制能力。
gateway网关的配置
需要的依赖
<!--服务的注册和发现-->
<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-gateway</artifactId>
</dependency><!--负载均衡-->
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
注意:不要同时引入 spring-boot-starter-web 和 spring-cloud-starter-gateway:
spring-cloud-starter-gateway:是基于 WebFlux 的响应式编程模型
spring-boot-starter-web: 是基于 Servlet 的传统 MVC 模型
两者使用不同的技术栈,同时引入会导致启动冲突网管也是一个服务,也需要被注册到nacos中
集群的情况要使用动态路由并且一定要引入loadbalancer依赖,要不然在有多个实例的情况下网关不知道路由到那个实例会报错
网关的yml文件配置
注册与发现配置:
spring:application:name: service-gatewaycloud:nacos:discovery:server-addr: 127.0.0.1:8848namespace: you-namespacegroup: DEFAULT_GROUPusername: you-usernamepassword: you-password
注意要在启动类上加上 @EnableDiscoveryClient注解
网关路由配置
路由配置主要是4个核心参数:
- id:一个路由的唯一标识,请保证他的唯一的,相当于主键,可以设置为和服务名一致。
- uri:可以理解为是通过条件匹配之后需要路由到(跳转,重定向)到的新的服务地址。
- predicates:断言,当客户端访问请求满足该规则时才进行uri。
- filters:可以理解为是在网关路由到对应服务前对请求进行额外的其他操作,例如拼接或者裁减等。
假设本地启动了除网关外的另外三个服务
- service-consumer1:9991 服务地址:127.0.0.1:9991/
- service-consumer2:9992 服务地址:127.0.0.1:9992/
- service-consumer3:9993 服务地址:127.0.0.1:9993/
静态路由配置示例:
spring:cloud:
#服务注册与发现配置nacos:discovery:server-addr: 127.0.0.1:8848namespace: you-namespacegroup: DEFAULT_GROUPusername: you-usernamepassword: you-password
#网关路由配置gateway:routes:- id: service-consumer1 uri: http://127.0.0.1:9991 predicates:- Path=/service-consumer1/** - id: service-consumer2 uri: http://127.0.0.1:9992 predicates:- Path=/service-consumer2/** - id: service-consumer3 uri: http://127.0.0.1:9993predicates:- Path=/service-consumer3/**
就拿id为service-consumer1的路由来举例:
假设客户端发送请求:http://127.0.0.1:8080/service-consumer1/test/getInfo
它的请求路径为:/service-consumer1/test/getInfo
我们发现他满足第一个路由规则(predicates:- path: /service-consumer1/**),返回true
之后网关会将http://127.0.0.1:8080/service-consumer1/test/getInfo路由成http://127.0.0.1:9991/service-consumer1/test/getInfo
注意:路由后的最终路径是:uri(http://127.0.0.1:9991) + 请求路径(/service-consumer1/test/getInfo)
那我们想啊我实际service-consumer1的服务地址是127.0.0.1:9991/,对应某个接口路径是127.0.0.1:9991/test/getInfo,并没有service-consumer1呀,此时我们可以通过ilters: - StripPrefix=1来删除断言的前缀,如下:
spring:cloud:
#服务注册与发现配置nacos:discovery:server-addr: 127.0.0.1:8848namespace: you-namespacegroup: DEFAULT_GROUPusername: you-usernamepassword: you-password
#网关路由配置gateway:routes:- id: service-consumer1 uri: http://127.0.0.1:9991 predicates:- Path=/service-consumer1/** filters:- StripPrefix=1
此时网关会将http://127.0.0.1:8080/service-consumer1/test/getInfo路由成http://127.0.0.1:9991/test/getInfo
动态路由配置示例:
spring:cloud:#服务注册与发现配置nacos:discovery:server-addr: 127.0.0.1:8848namespace: you-namespacegroup: DEFAULT_GROUPusername: you-usernamepassword: you-password#网关路由配置gateway:routes:- id: service-consumer1 uri: lb://service-consumer1predicates:- Path=/service-consumer1/** filters:- StripPrefix=1 - id: service-orderuri: lb://service-consumer2predicates:- Path=/service-consumer2/**filters:- StripPrefix=1- id: service-stockuri: lb://service-consumer3predicates:- Path=/service-consumer3/**filters:- StripPrefix=1
与静态路由配置的不同在于uri,动态路由配置uir格式为lb://服务名称
lb表示负载均衡的意思,当对应服务器集群的情况下会通过负载均衡规则选择一台机器
服务名称是实际注册到nacos的服务名称,gateway会通过服务名称到nacos服务列表里面找到对应的服务域名和端口进行路由。
为什么叫动态路由配置呢,是因为gateway里面只需要配置服务名称,尽管该服务域名或者端口发生了改变, gateway通过服务名称查找也能动态路由到对应的地址
Spring Cloud Gateway 的路由匹配机制遵循以下原则:
- 从上到下匹配:路由按照在配置文件中定义的顺序进行匹配。
- 找到第一个匹配就停止:一旦找到第一个满足所有断言条件的路由,就使用该路由进行处理,不会继续匹配后续路由,即使后续路由也能匹配,也不会继续匹配
其他扩展参数配置
断言参数(predicates)
上面配置中我们只配置了path一个predicates参数用于匹配URL相关路径,实际predicates还有很多参数配置,比如:
After
例如:- After=2021-01-01
匹配在2021年一月一日时间之后发生的请求。
- Before
例如:- Before=2021-01-01
匹配在2021年一月一日时间之前发生的请求。
Between
例如:- Before=2021-01-01,2021-01-02
匹配在2021年一月一日至2021年一月二日之间发生的请求。
Cookie
例如:- Cookie=demokey, abc
cookie的设置,有两个参数,分别是name和regexp(Java正则),可以匹配到相应名称的Cookie名称,且与正则相匹配的Cookie值的链接。
Header
例如:- Header=X-Request-Id, \d+
Header同样也提供了两个参数,分别是name和regexp(Java正则),可以匹配相应类型的Url,比如127.0.0.1/demo/1,这样就可以进入上述规则。
Host
例如:- Host=**.pxyz.top
Host就比较好理解了,其参数就是匹配相应的ip,或者域名等信息的Url。
Method
例如:- Method=GET
Method就更加熟悉了,GET、POST、PUT、DELETE等都是属于Method中的一类,上述就是匹配GET类的请求。
Query
例如:- Query=abc
Query:查询条件,用于匹配查询条件是否存在abc条件。
RemoteAddr
例如:- RemoteAddr=192.168.1.1/24
RemoteAddr:指定请求远程地址IPIP,可以设置多个网段等功能
Weight
例如:- Weight= gtoup, 2
利用路由权重来匹配对应的路由规则。
........
- 自定义Predicate
上面都是一些原生的内置参数,如果生产环境而言这些配置还不能满足开发需求,就需要自定义Predicate
自定义断言类名需要以RoutePredicateFactory结尾,并继承AbstractRoutePredicateFactory类。
示例:
@Component
public class UserTypeRoutePredicateFactory extends AbstractRoutePredicateFactory<UserTypeRoutePredicateFactory.Config> {public UserTypeRoutePredicateFactory() {super(UserTypeRoutePredicateFactory.Config.class);}@Overridepublic Predicate<ServerWebExchange> apply(UserTypeRoutePredicateFactory.Config config) {return serverWebExchange -> {String userType = serverWebExchange.getRequest().getHeaders().getFirst("userType");if (Objects.isNull(userType)){return false;}return userType.equalsIgnoreCase(config.getAuthType());};}//支持Shortcut Configuration配置@Overridepublic List<String> shortcutFieldOrder(){return Collections.singletonList("authType");}@Datapublic static class Config {/*** 0-普通用户 1-普通管理员 2-超级管理员*/private String authType;}
}
在网关的yml文件中配置上自定义断言就可以使用了:
注意:yml文件中配置自定义断言名称就是自定义Predicate类名称RoutePredicateFactory前面的部分,比如我的自定义Predicate类名为UserTypeRoutePredicateFactory,那么yml里面配置的自定义断言名称属于名为UserType.注意大小写,必须一模一样
代码解释:
注意不要少了@Componet注解否则无法自动配扫描到
重写apply方法
处理断言匹配业务逻辑,比如上面是从请求头里面获取userType与Config.userType进行比较,相等则匹配正确。
Config类
用于定义和存储路由断言的配置参数,与 shortcutFieldOrder 方法配合使用,将配置文件里面的参数映射到Config属性里面。
shortcutFieldOrder方法的作用:
支持快捷配置(Shortcut Configuration),它定义了在YML配置文件中使用简化的参数配置方式。
- 启用简写配置语法:有了这个方法后,可以在路由配置中使用 UserType=3 这种简写形式,而不需要使用复杂的 name: UserType args: {userType: 3} 形式
- 参数映射:指定 "authType" 字符串告诉Spring Cloud Gateway,当使用 UserType=value 语法时,应该将 value 赋值给 Config 对象的 authType字段
同一个断言Predicate下的参数列表默认都是“且”的关系,必须同时满足才可以返回true,否则返回false,我们也可以通过上面的自定义Predicate来实现“或”的关系,这边就不举例了,自行扩展实现。
过滤器参数
只有满足对应的Predicate才会执行过滤器,如果断言返回false则不会执行过滤器
上面配置中我们只配置了StripPrefix一个filters参数用于移除请求路径的前缀,实际是filters也有很多,比如:
AddRequestHeader
:添加请求头。例如:-
AddRequestHeader:token,vsdgggft21323643
表示请求头添加一个token属性,值为
vsdgggft21323643,如果需要添加多个请求头则重写一行,比如:
-
AddRequestHeader:token,vsdgggft21323643
-
AddRequestHeader:auth,1223223434f5f45544
RemoveRequestHeader:移除请求头
例如:- RemoveRequestHeader:
auth
表示移除请求头里面auth属性
SetRequestHeader:设置请求头某个属性的值
例如:- SetRequestHeader:auth,2132132445454546
表示将请求头里面auth的值改成2132132445454546
AddRequestParameter
:添加请求参数。RemoveRequestParameter:移除请求参数
AddResponseHeader:添加响应头
SetResponseHeader:设置响应头属性值
RemoveResponseHeader:移除响应头
SetPath
例如:
比如:客户端请求:http:127.0.0.1:8080/service-provider/test/info
占位符{path}=/test/info,所以实际路由的地址是127.0.0.1:service-provider服务的端口号/service-consumer/test/info
RedirectTo:重定向
RewritePath
:重写请求路径。
CircuitBreaker
:熔断器,用于防止服务雪崩。
RateLimiter
:限流器,控制请求的速率。........
上面都是一些内置的配置的过滤器和路由配置,Spring Cloud Gateway
还支持通过 Java
配置类来自定义过滤器和路由。这为实现特定业务逻辑提供了灵活性。
自定义全局Filter
要实现全局过滤器需要实现GlobalFilter
示例:请求、响应信息日志打印,记录请求总时长
@Slf4j
@Component
public class GlobalLogFilter implements GlobalFilter, Ordered {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {exchange.getAttributes().put("startTime",System.currentTimeMillis());return chain.filter(exchange).then(Mono.fromRunnable(()->{log.info("请求头:{}",exchange.getRequest().getHeaders());log.info("请求路径:{}",exchange.getRequest().getPath());log.info("请求方法:{}",exchange.getRequest().getMethod());log.info("请求参数:{}",exchange.getRequest().getQueryParams());log.info("响应头:{}",exchange.getResponse().getHeaders());log.info("响应状态:{}",exchange.getResponse().getStatusCode());Long startTime = exchange.getAttribute("startTime");if (startTime != null) {log.info("请求总耗时:{}ms", System.currentTimeMillis() - startTime);}}));}@Overridepublic int getOrder() {return 0;}
}
控制台:
order是定义过滤器的执行顺序,当存在多个过滤器时,order越小优先级越高越先执行 ,也可以通过@Order注解实现
只要在网关yml配置的所有路由,且可以成功匹配断言路由到对应服务器的都会执行全局过滤器
自定义局部Filter
局部过滤器的命名必须是XXXGatewayFilterFactory
示例:
@Component
@Slf4j
@Order(1)
public class AuthFilterGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthFilterGatewayFilterFactory.AuthConfig> {public AuthFilterGatewayFilterFactory() {super(AuthFilterGatewayFilterFactory.AuthConfig.class);}@Overridepublic List<String> shortcutFieldOrder() {return Arrays.asList("authType", "serverCode");}@Overridepublic GatewayFilter apply(AuthFilterGatewayFilterFactory.AuthConfig config) {log.info("开始执行局部过滤器");return (exchange, chain) -> {// 获取请求头中的用户类型String authType = exchange.getRequest().getHeaders().getFirst("authType");// 检查用户类型是否匹配配置的权限类型if (authType == null || !authType.equals(config.getAuthType())) {ServerHttpResponse response = exchange.getResponse();response.setStatusCode(HttpStatus.UNAUTHORIZED);String errorMsg = "权限不足,需要权限类型: " + config.getAuthType();log.warn("访问拒绝: {}", errorMsg);return response.setComplete();}// 检查服务编码(可以根据需要自定义逻辑)String ServerCode = exchange.getRequest().getHeaders().getFirst("serverCode");if (config.getServerCode() != null && !config.getServerCode().equals(ServerCode)) {ServerHttpResponse response = exchange.getResponse();response.setStatusCode(HttpStatus.FORBIDDEN);String errorMsg = "服务编码不匹配,需要服务编码: " + config.getServerCode();log.warn("服务访问拒绝: {}", errorMsg);return response.setComplete();}// 验证通过,继续执行下一个过滤器log.info("局部过滤器结束");return chain.filter(exchange);};}@Datapublic static class AuthConfig {/*** 用户权限类型*/private String authType;/*** 服务编码*/private String serverCode;}
}
网关yml配置,给某个路由添加过滤器:
其实这个在配置上跟自定义Predicate很像
配置上name的值就是自定义过滤器XXXGatewayFilterFactory 的前缀xxx,比如我的自定义局部过滤器AuthFilterGatewayFilterFactory ,name就是AuthFilte
AuthConfig就作用就是存储args的,与shortcutFieldOrder方法配合使用,将args的参数映射到AuthConfig上
只有添加了对应的局部过滤器的路由,在访问时才会执行对应的局部过滤器,否则不执行
补充:关于全局过滤器和局部过滤器的执行顺序问题
过滤器的执行顺序并不会因为是全局过滤器还是局部过滤器而分先后,它只跟order的值有关,越小优先级越高越先执行,order的值可以为负数,相同优先级时按照添加顺序执行
过滤器分为两个阶段:
pre 阶段:请求转发到目标服务之前,按照优先级从高到低执行
post 阶段:响应返回给客户端之前,按照优先级低从到高执行