网关Gateway
Gateway是在Spring生态系统之上构建的API网关服务,基于Spring6,Spring Boot 3和Project Reactor等技术。它旨在为微服务架构提供一种简单有效的统一的API路由管理方式,并为它们提供跨领域的关注点,例如:安全性、监控/度量和恢复能力。
1. Gateway概述
在1.x版本中都是采用的Zuul网关;但在2.x版本中,zuul的升级一直跳票,SpringCloud最后自己研发了一个网关SpringCloud Gateway替代Zuul。
1.1 Gateway在微服务作用
不同于Nginx, Spring Cloud Gateway相当于去医院看病中的分诊台(用于签到等),Nginx用于挂号。
- 反向代理
- 鉴权
- 流量控制
- 熔断
- 日志监控
1.2 Gateway总结
Spring Cloud Gateway组件的核心是一系列的过滤器,通过这些过滤器可以将客户端发送的请求转发(路由)到对应的微服务。Spring Cloud Gateway是加在整个微服务最前沿的防火墙和代理器,隐藏微服务结点IP端口信息,从而加强安全保护。Spring Cloud Gateway本身也是一个微服务,需要注册进服务注册中心。
2. Gateway三大核心
2.1 Route(路由)
路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成。如果断言为true则匹配该路由。
2.2 Predicate(断言)
开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由。
2.3 Filter(过滤)
指的是Spring框架中GatewayFilter的实例,使用过滤器。可以在请求被路由前或者之后对请求进行修改。
提示
前端请求,通过一些匹配条件,定位到真正的服务节点。并在这个转发过程的前后,进行一些精细化控制。predicate就是我们的匹配条件;filter,就可以理解为一个无所不能的拦截器。有了这两个元素,再加上目标uri,就可以实现一个具体的路由了。
3. Gateway工作流程
客户端向Spring Cloud Gateway发出请求。然后在Gateway Handler Mapping中找到与请求相匹配的路由,将其发送到Gateway Web Handler。Handler再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。
过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(Pre)或之后(Post)执行业务逻辑。
在"pre"类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等;
在"post"类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用。
4. 入门配置
4.1 新建子工程cloud-gateway9527
点击File->New->Module:
4.2 添加依赖
<dependencies>
<!--gateway-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--服务注册发现consul discovery,网关也要注册进服务注册中心统一管控-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<!-- 指标监控健康检查的actuator,网关是响应式编程删除掉spring-boot-starter-web dependency-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
4.3 编辑application.yml
server:
port: 9527
spring:
application:
name: cloud-gateway #以微服务注册进consul或nacos服务列表内
cloud:
consul: #配置consul地址
host: localhost
port: 8500
discovery:
prefer-ip-address: true
service-name: ${spring.application.name}
4.4 新建启动类
@SpringBootApplication
@EnableDiscoveryClient //服务注册和发现
public class Main9527 {
public static void main(String[] args) {
SpringApplication.run(Main9527.class, args);
}
}
4.5 路由映射配置
在cloud-gateway9527工程的resource目录下新建application.yml:
server:
port: 9527
spring:
application:
name: cloud-gateway #以微服务注册进consul或nacos服务列表内
cloud:
consul: #配置consul地址
host: localhost
port: 8500
discovery:
prefer-ip-address: true
service-name: ${spring.application.name}
gateway:
routes:
- id: pay_path1 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/get/** # 断言,路径相匹配的进行路由
- id: pay_path2
uri: http://localhost:8001
predicates:
- Path=/pay/gateway/info/**
4.6 在cloud-provider-payment8001工程中添加Controller
新增OrderGateWayController:
@RestController
public class OrderGateWayController {
@Resource
private PayGatewayFeignApi payGatewayFeignApi;
@GetMapping(value = "/consumer/pay/gateway/get/{id}")
public ResultData getById(@PathVariable("id") Long id) {
return payGatewayFeignApi.getById(id);
}
@GetMapping(value = "/consumer/pay/gateway/info")
public ResultData<String> getGatewayInfo() {
return payGatewayFeignApi.getGatewayInfo();
}
}
4.7 在cloud-api-commons工程添加接口
新增类PayGatewayFeignApi:
// 使用cloud-gateway网关
@FeignClient(value = "cloud-gateway")
public interface PayGatewayFeignApi {
@GetMapping(value = "/pay/gateway/get/{id}")
public ResultData getById(@PathVariable("id") Long id);
@GetMapping(value = "/pay/gateway/info")
public ResultData<String> getGatewayInfo();
}
4.8 在cloud-consumer-feign-order80工程添加接口
新增OrderGateWayController:
@RestController
public class OrderGateWayController {
@Resource
private PayGatewayFeignApi payGatewayFeignApi;
@GetMapping(value = "/consumer/pay/gateway/get/{id}")
public ResultData getById(@PathVariable("id") Long id) {
return payGatewayFeignApi.getById(id);
}
@GetMapping(value = "/consumer/pay/gateway/info")
public ResultData<String> getGatewayInfo() {
return payGatewayFeignApi.getGatewayInfo();
}
}
4.9 测试代码
调用接口/consumer/pay/gateway/get/1,发现接口调用通过: 关闭cloud-gateway工程,再次调用接口/consumer/pay/gateway/get/1, 发现调用不通过,网关发挥作用:
5. Gateway高级特性
5.1 动态获取URI
可以使用SpringCloud ReactorLoadBalancer去解析
lb:服务名称
形式来代替IP地址+端口。
修改在cloud-gateway9527工程application.yml:
server:
port: 9527
spring:
application:
name: cloud-gateway #以微服务注册进consul或nacos服务列表内
cloud:
consul: #配置consul地址
host: localhost
port: 8500
discovery:
prefer-ip-address: true
service-name: ${spring.application.name}
gateway:
routes:
- id: pay_routh1 #pay_routh1 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/get/** # 断言,路径相匹配的进行路由
- id: pay_routh2 #pay_routh2 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/info/** # 断言,路径相匹配的进行路由
5.2 测试
6. Predicate断言(谓词)
路由断言是在Spring Cloud Gateway中管理请求路由的核心机制。通过对请求的各种属性进行匹配,Spring Cloud Gateway可以灵活地将请求转发到正确的微服务。使用断言,你可以实现复杂的路由逻辑,同时保持系统的高效性和可维护性。
6.1 配置断言方式
- 快捷配置
使用=间隔,左边是断言类型,右边是断言参数,多个参数用逗号隔开。比如:
predicates:
- Cookie=mycookie,mycookievalue
- 完全展开的参数配置
类似YAML格式,使用map结构,其中一组是key为name,value是断言类型;另外一组是key为args,value是断言参数。
predicates:
- name: Cookie
args:
name: mycookie
regexp: mycookievalue
6.2 常用的断言
cloud-gateway9527工程启动会发现日志默认有支持以下断言:
- After Route Predicate
predicates:
- After=2024-10-31T21:58:39.423821200+08:00[Asia/Shanghai]
表示匹配指定日期之后的请求允许放行,After之后的参数日期可以通过System.out.println(ZonedDateTime.now())
获得。
2. Before Route Predicate
predicates:
- Before=2024-10-31T21:58:39.423821200+08:00[Asia/Shanghai]
表示匹配指定日期之前的请求允许放行,也就是超过规定时间不可访问,Before之后的参数日期可以通过System.out.println(ZonedDateTime.now())
获得。
3. Between Route Predicate
predicates:
- Between=2024-10-31T21:58:39.423821200+08:00[Asia/Shanghai], 2024-10-31T21:58:49.423821200+08:00[Asia/Shanghai]
Between断言包含两个日期参数,用逗号隔开,并且第一个日期参数必须小于第二个日期参数,表示请求满足大于第一日期并小于第二日期才放行。
4. Cookie Route Predicate
predicates:
- Cookie=name,jack
需要两个参数,一个是Cookie name ,一个是正则表达式。路由规则会通过获取对应的Cookie name值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上则不执行。
验证方式1: 使用原生命令--curl http://localhost:9527/pay/gateway/get/1 --cookie "username=zzyy"
验证方式2: 使用Postman
验证方式3: chrome浏览器 5. Header Route Predicate
两个参数:一个是请求头名称和一个正则表达式,这个属性值和正则表达式匹配则执行。比如:
predicates:
- Header=X-Request-Id, \d+
- Host Route Predicate
参数名称固定为Host, 接收一组参数, 参数里面为匹配的域名列表, 多个域名用逗号隔开, 域名里面使用ant语法匹配。比如:
predicates:
- Host=**.baidu.com,**.jack.com
- Path Route Predicate
请求路径匹配规则,多个路径使用逗号隔开,比如
predicates:
- Path=/pay/gateway/get/**, /pay/gateway/getInfo/**
- Query Route Predicate
传入两个参数,一个是参数名,一个为参数值,参数值可以是正则表达式。比如:
predicates:
- Query=red, gree.
- RemoteAddr Route Predicate 外部访问进行IP限制规则,最大跨度不超过32,目前是1~24它们是CIDR(无类别域间路由Classless Inter-Domain Routing缩写)表示法。
predicates:
- RemoteAddr=192.168.1.1/24 # 表示192.168.1.0~255范围放行,否则404
- Method Route Predicate
请求方式为GET还是POST进行规则限制,比如:
predicates:
- Method=GET,POST
6.3 自定义规则断言
如果上述规则不满足需要,自定义断言有两个方式:
- 继承AbstractRoutePredicateFactory抽象类(推荐)
- 实现RoutePredicateFactory接口
提示
命名需要注意的是开头任意取名,但是必须以RoutePredicateFactory后缀结尾。
- 在cloud-gateway9527工程中添加TokenRoutePredicateFactory类:
/**
* 1. 集成AbstractRoutePredicateFactory抽象类, 使用@Component注解
*/
@Component
public class TokenRoutePredicateFactory
extends AbstractRoutePredicateFactory<TokenRoutePredicateFactory.Config> {
// 4. 无参构造调用父类
public TokenRoutePredicateFactory() {
super(TokenRoutePredicateFactory.Config.class);
}
// 5. 重写shortcutFieldOrder来支持快捷配置方式,默认只支持完全展开的参数配置,
@Override
public List<String> shortcutFieldOrder() {
return Collections.singletonList("token");
}
/**
* 2. 重写apply方法, 匹配规则逻辑实现
* @param config
* @return
*/
@Override
public Predicate<ServerWebExchange> apply(TokenRoutePredicateFactory.Config config) {
return serverWebExchange -> {
HttpHeaders headers = serverWebExchange.getRequest().getHeaders();
String token = headers.getFirst("Token");
// 模拟校验
if(StringUtils.hasText(config.token)){
if(config.token.equals(token)){
return true;
}else {
return false;
}
}else{
return false;
}
};
}
// 3. 创建路由断言规则信息的实体
public static class Config {
private String token;
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
}
}
- 配置application.yml:
server:
port: 9527
spring:
application:
name: cloud-gateway #以微服务注册进consul或nacos服务列表内
cloud:
consul: #配置consul地址
host: localhost
port: 8500
discovery:
prefer-ip-address: true
service-name: ${spring.application.name}
gateway:
routes:
- id: pay_path1 #路由的ID(类似mysql主键ID),没有固定规则但要求唯一,建议配合服务名
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/pay/gateway/get/** # 断言,路径相匹配的进行路由
- Token=admin@124
- id: pay_path2
uri: lb://cloud-payment-service
predicates:
- Path=/pay/gateway/info/**
- 启动cloud-gateway9527工程:
- 测试接口,如果token不对会直接404:
- 测试接口,如果token正确,也就是和配置一致:
7. Filter过滤
7.1 Filter过滤器概述
主要用来请求鉴权,异常处理,记录接口调用时长等,底层是SpringMVC里面的的拦截器Interceptor、 Servlet的过滤器, "pre"和"post"分别会在请求被执行前调用和被执行后调用, 用来修改请求和响应信息。一共有三种类型:
- 全局默认过滤器Global Filters, 不需要配置文件中配置, 实现GlobalFilter接口即可作用全部路由。
- 单一内置过滤器GatewayFilter, 也被称为网关过滤器,主要用于单一路由或者某个路由分组。
- 自定义过滤器
7.2 内置Filter过滤器
- 请求头(RequestHeader)相关组
- 添加指定请求头
filters:
- AddRequestHeader=X-Request-red, blue
- AddRequestHeader=X-Request-test, test # 多个请求头可以多行添加
- 修改指定请求头
filters:
- SetRequestHeader=sec-fetch-mode, Blue-updatebyzzyy # 将请求头sec-fetch-mode对应的值修改为Blue-updatebyzzyy
- 删除指定请求头
filters:
- RemoveRequestHeader=sec-fetch-site # 删除请求头sec-fetch-site
- 请求参数(RequestParameter)相关组
- 添加请求参数
filters:
- AddRequestParameter=customerId,9527001 # 新增请求参数Parameter:k,v
- 删除请求参数
filters:
- RemoveRequestParameter=customerName # 删除url请求参数customerName,你传递过来也是null
- 响应头(ResponseHeader)相关组
- 添加响应头
filters:
- AddResponseHeader=X-Response-test, BlueResponse # 新增请求参数X-Response-test并设值为BlueResponse
- 修改响应头
filters:
- SetResponseHeader=Date,2099-11-11 # 设置回应头Date值为2099-11-11
- 删除响应头
filters:
- RemoveResponseHeader=Content-Type # 将默认自带Content-Type回应属性删除
- 前缀和路径相关组,常用来对外隐藏真实地址
- 访问路径添加前缀
filters:
- Path=/gateway/filter/**
- PrefixPath=/pay # 实际访问路径=PrefixPath + Path, 也就是http://localhost:9527/pay/gateway/filter
- 访问路径替换
filters:
- Path=/XYZ/abc/{segment} # {segment}表示占位符,替换的时候被保留
- SetPath=/pay/gateway/{segment} # SetPath中的路径将会把Path的路径完全替换,比如浏览器访问地址: http://localhost:9527/XYZ/abc/filter, 实际微服务地址:http://localhost:9527/pay/gateway/filter,其中filter会被识别为占位符,继续保留
- 重定向地址
filters:
- Path=/pay/gateway/filter/** # 匹配地址
- RedirectTo=302, http://www.baidu.com/ # 访问http://localhost:9527/pay/gateway/filter跳转到http://www.baidu.com/
- 默认过滤器 默认过滤器和全局过滤器作用相同,作用在全局上:
spring:
cloud:
gateway:
default-filters:
- AddRequestHeader=X-Request-Default-Red
7.3 自定义过滤器
- 自定义全局Filter
新建MyGlobalFilter类并实现GlobalFilter,Ordered两个接囗:
@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {
Logger log = LoggerFactory.getLogger(MyGlobalFilter.class);
// 数字越小优先级越高
@Override
public int getOrder(){
return 0;
}
private static final String BEGIN_VISIT_TIME = "begin_visit_time";//开始访问时间
/**
* 接口统计
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//先记录下访问接口的开始时间
exchange.getAttributes().put(BEGIN_VISIT_TIME, System.currentTimeMillis());
return chain.filter(exchange).then(Mono.fromRunnable(()->{
Long beginVisitTime = exchange.getAttribute(BEGIN_VISIT_TIME);
if (beginVisitTime != null){
log.info("访问接口主机: " + exchange.getRequest().getURI().getHost());
log.info("访问接口端口: " + exchange.getRequest().getURI().getPort());
log.info("访问接口URL: " + exchange.getRequest().getURI().getPath());
log.info("访问接口URL参数: " + exchange.getRequest().getURI().getRawQuery());
log.info("访问接口时长: " + (System.currentTimeMillis() - beginVisitTime) + "ms");
log.info("我是美丽分割线: ###################################################");
}
}));
}
}
- 自定义条件Filter
新建MyGatewayFilterFactory类,继承AbstractGatewayFilterFactory
@Component
public class MyGatewayFilterFactory extends AbstractGatewayFilterFactory<MyGatewayFilterFactory.Config> {
@Override
public GatewayFilter apply(MyGatewayFilterFactory.Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
System.out.println("进入自定义网关过滤器MyGatewayFilterFactory,status===="+config.getStatus());
if(request.getQueryParams().containsKey("jack")) {
return chain.filter(exchange);
}else {
exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
return exchange.getResponse().setComplete();
}
};
}
//设定一个状态值/标志位,它等于多少,匹配和才可以访问
public static class Config {
private String status;
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
}
}
提示
命名需要注意的是开头任意取名,但是必须以GatewayFilterFactory后缀结尾。