代码地址

4. gateway 服务网关

官网入口

4.1 什么是网关

在一个大型项目中,一个系统会被拆分成很多微服务,如果从外部入口直达每个微服务就会有诸多问题

  • 客户端需要在每个微服务进行认证
  • 跨域请求,存在一定复杂性
  • 客户端出现代码和配置重复

所以有了网关这种东西, 就是在服务前面有一个统一入口,将一些与业务无关的公共逻辑封装到里面包括认证、鉴权、监控、路由、限流等等。

image.png

4.2 gateway简介

Spring Cloud Gateway是Spring家族开发代替zuul的统一API路由管理方式。并且基于Filter提供网关基本功能,安全,监控,限流。

4.2.1 Route

  1. 路由:就是字面意思,满足条件后转发到某个微服务,一个gateway可以包含多个路由。

4.2.2 Predicate

  1. 断言:转发前的特定条件

4.2.3 Filter

  1. 过滤:在数据传递过程中通过 过滤器做一些事情。

4.2.4 流程

客户端访问 gateway网关,gateway中Handler Mappeer对url进行处理。处理后交给Web Handler ,Web Handler 会被Filter进行过滤,Filter前半部分会对请求的部分进行过滤,请求后被真实服务代理响应返回结果,Filter后半部分回对结果进行操作,然后给Web Handler,再返回给Handler Mapper。最终返回给客户端。

image.png

4.3 构建工程

可以单独使用gateway 也可以将gateway和其他服务一起注册到注册中心 然后从注册中心自动获取地址

4.3.1 动态路由 自动获取URL

加入负载均衡组件 可以自动轮询服务
pom.xml

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5. <parent>
  6. <artifactId>cloudalibaba</artifactId>
  7. <groupId>com.rem</groupId>
  8. <version>2021</version>
  9. </parent>
  10. <modelVersion>4.0.0</modelVersion>
  11. <artifactId>api-gateway</artifactId>
  12. <dependencies>
  13. <dependency>
  14. <groupId>org.springframework.cloud</groupId>
  15. <artifactId>spring-cloud-starter-gateway</artifactId>
  16. </dependency>
  17. <dependency>
  18. <groupId>com.alibaba.cloud</groupId>
  19. <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
  20. </dependency>
  21. <dependency>
  22. <groupId>org.springframework.cloud</groupId>
  23. <artifactId>spring-cloud-starter-bootstrap</artifactId>
  24. </dependency>
  25. <!--负载均衡-->
  26. <dependency>
  27. <groupId>org.springframework.cloud</groupId>
  28. <artifactId>spring-cloud-starter-loadbalancer</artifactId>
  29. </dependency>
  30. </dependencies>
  31. </project>

bootstrap.yml

  1. server:
  2. port: 9000
  3. spring:
  4. application:
  5. name: api-gateway
  6. cloud:
  7. nacos:
  8. discovery:
  9. namespace: 3d0a77b8-817f-499b-bfda-f90d5a6e4dab
  10. server-addr: 192.168.19.128:13306
  11. group: DEFAULT_GROUP
  12. username: nacos
  13. password: nacos
  14. gateway:
  15. routes: # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
  16. - id: provider-product # 当前路由的标识, 要求唯一
  17. uri: lb://provider-product # lb指的是从nacos中按照名称获取微服务,并遵循负载均衡策略 #http://127.0.0.1:7001
  18. predicates: # 断言(就是路由转发要满足的条件)
  19. - Path=/product/** # 当请求路径满足Path指定的规则时,才进行路由转发

当请求 http://127.0.0.1:9000/product/test/1 的时候就会自动轮询7001和7002两个product服务

4.3.2 动态路由 服务名称转发

新增动态配置

  1. gateway:
  2. discovery:
  3. locator:
  4. #是否与服务发现相结合,通过serviceId转发到具体服务
  5. enabled: true

这样就不需要配置routes的配置 不需要直接写微服务实际地址直接根据服务名就可以,通过服务名称访问
http://127.0.0.1:9000/provider-product/product/test/1

4.4 断言

官方提供11种断言方法,也可自定义断言方法

4.4.1 After Route Predicate Factory

接收一个日期参数,判断请求日期是否晚于指定日期,访问时间在2020-12-31 23:59:59之后所有请求都通过

  1. spring:
  2. cloud:
  3. gateway:
  4. routes:
  5. - id: provider-product
  6. uri: http://127.0.0.1:7001
  7. predicates:
  8. - After=2020-12-31T23:59:59.789+08:00[Asia/Shanghai]

4.4.2 Before Route Predicate Factory

接收一个日期参数,判断请求日期是否早于指定日期,访问时间在2020-12-31 23:59:59之前所有请求都通过

  1. predicates:
  2. - Before=2020-12-31T23:59:59.789+08:00[Asia/Shanghai]

4.4.3 Between Route Predicate Factory

接收两个日期参数,判断请求日期是否在指定时间段内,访问时间在2020-12-31 23:59:59至2022-12-31 23:59:59中间会通过

  1. predicates:
  2. - Between=2020-12-31T23:59:59.789+08:00[Asia/Shanghai],2022-12-31T23:59:59.789+08:00[Asia/Shanghai]

4.4.4 Cookie Route Predicate Factory

接收两个参数,Cookie 名字和一个正则表达式。
当只有一个Cookie name变量时,当输入带有Cookie name时就能通过,带有正则表达式的时候判断name和正则表达式是否匹配 成功才能通过

  1. predicates:
  2. - Cookie=chocolate, ch.p

4.4.5 Header Route Predicate Factory

接收两个参数,和Cookie一样,可以输入一个参数也可输入两个参数 判断Header是否与给定值匹配。

  1. predicates:
  2. - Header=X-Request-Id, \d+

4.4.6 Host Route Predicate Factory

接收一个参数,主机名模式。判断请求的Host是否满足匹配规则

  1. predicates:
  2. -Host=**.testhost.org

4.4.7 Method Route Predicate Factory

接收一个参数,判断请求类型是否跟指定的类型匹配,只有get或者post的请求才能通过

  1. predicates:
  2. - Method=GET,POST

4.4.8 Path Route Predicate Factory

接收一个参数,判断请求的URI部分是否满足路径规则,配合filter使用 例如
/api/product/test/1 转发到product时为 /product/test/1

  1. predicates:
  2. - Path=/api/**
  3. filters:
  4. - StripPrefix=1 # 转发之前 去掉1层路径

4.4.9 Query Route Predicate Factory

接收两个参数,请求param和正则表达式, 判断请求参数是否具有给定名称且值与正则表达式匹配

  1. predicates:
  2. - Query=nnname,abc. # .代表任意字符

4.4.10 The RemoteAddr Route Predicate Factory

接收一个IP地址段,判断请求主机地址是否在地址段中,不在指定地址中将访问失败

  1. predicates:
  2. - RemoteAddr=172.16.1.191/0 #0表示子网掩码

4.4.11 The Weight Route Predicate Factory

接收一个[组名,权重], 然后对于同一个组内的路由按照权重转发
product是两个副本 访问 http://localhost:9000/product/test/2 这样就每三次访问7002才访问一次7001

  1. routes:
  2. - id: provider-product
  3. uri: http://127.0.0.1:7001
  4. predicates:
  5. - Weight=mygroup, 1
  6. - id: provider-product2
  7. uri: http://127.0.0.1:7002
  8. predicates:
  9. - Weight=mygroup, 3

4.4.12 自定义断言

所有断言都是 AbstractRoutePredicateFactory 接口所有也可以自定义一个断言实现这个接口
image.png
实现一个自定义断言,当满足一个年龄范围内才能通过

  1. package com.rem.cloudalibaba.predicate;
  2. import com.alibaba.nacos.common.utils.StringUtils;
  3. import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
  4. import org.springframework.stereotype.Component;
  5. import org.springframework.validation.annotation.Validated;
  6. import org.springframework.web.server.ServerWebExchange;
  7. import java.util.Arrays;
  8. import java.util.List;
  9. import java.util.function.Predicate;
  10. /**
  11. * 自定义断言工厂
  12. *
  13. * @author Rem
  14. * @date 2021-09-23
  15. */
  16. @Component
  17. public class AgeRoutePredicateFactory extends AbstractRoutePredicateFactory<AgeRoutePredicateFactory.Config> {
  18. public AgeRoutePredicateFactory() {
  19. super(AgeRoutePredicateFactory.Config.class);
  20. }
  21. @Override
  22. public List<String> shortcutFieldOrder() {
  23. //这里的顺序要跟配置文件中的参数顺序一致
  24. return Arrays.asList("minAge", "maxAge");
  25. }
  26. /**
  27. * 判断传入的age 是否在设置的断言内
  28. *
  29. * @param config
  30. * @return
  31. */
  32. @Override
  33. public Predicate<ServerWebExchange> apply(AgeRoutePredicateFactory.Config config) {
  34. return exchange -> {
  35. //从exchange获取传入的参数
  36. String ageStr = exchange.getRequest().getQueryParams().getFirst("age");
  37. if (StringUtils.isNotEmpty(ageStr)) {
  38. int age = Integer.parseInt(ageStr);
  39. return age > config.getMinAge() && age < config.getMaxAge();
  40. }
  41. return false;
  42. };
  43. }
  44. @Validated
  45. public static class Config {
  46. private Integer minAge;
  47. private Integer maxAge;
  48. public Integer getMinAge() {
  49. return minAge;
  50. }
  51. public void setMinAge(Integer minAge) {
  52. this.minAge = minAge;
  53. }
  54. public Integer getMaxAge() {
  55. return maxAge;
  56. }
  57. public void setMaxAge(Integer maxAge) {
  58. this.maxAge = maxAge;
  59. }
  60. }
  61. }
  1. gateway:
  2. routes:
  3. - id: provider-product
  4. uri: lb://provider-product
  5. predicates:
  6. - Age=18,60 # 限制年龄只有在18到60岁之间的人能访问

访问 http://127.0.0.1:9000/product/test/1?**age=19** 就可以访问了 如果不带age就会出404

4.5 过滤

gateway 内部实现了很多过滤器

过滤器工厂 作用 参数
AddRequestHeader 为原始请求添加Header Header的名称及值
AddRequestParameter 为原始请求添加请求参数 参数名称及值
AddResponseHeader 为原始响应添加Header Header的名称及值
DedupeResponseHeader 剔除响应头中重复的值 需要去重的Header名称及去重策略
FallbackHeaders 剔除响应头中重复的值 Header的名称
PrefixPath 为原始请求路径添加前缀 前缀路径
PreserveHostHeader 为请求添加一个
preserveHostHeader=true的属性,路由过滤器会检查该属性以决定是否要发送原始的Host
RequestRateLimiter 用于对请求限流,限流算法为令牌桶 keyResolver、rateLimiter、
statusCode、denyEmptyKey、
emptyKeyStatus
RedirectTo 将原始请求重定向到指定的URL http状态码及重定向的url
RemoveRequestHeader 为原始请求删除某个Header Header名称
RemoveResponseHeader 为原始请求删除某个Header Header名称
RewritePath 为原始响应删除某个Header 原始路径正则表达式以及重写后路径的正则表达式
RewriteResponseHeader 重写原始的请求路径 原始路径正则表达式以及重写后路径的正则表达式
SaveSession 在转发请求之前,强制执行WebSession::save 操作
secureHeaders 为原始响应添加一系列起安全作用的响应头 无,支持修改这些安全响应头的值
SetPath 修改原始的请求路径 修改原始的请求路径修
SetResponseHeader 修改原始响应中某个Header的值 Header名称,修改后的值
SetStatus 修改原始响应的状态码 HTTP 状态码,可以是数字,也可以是字符串
StripPrefix 用于截断原始请求的路径 使用数字表示要截断的路径的数量
Retry 针对不同的响应进行重试 retries、statuses、methods、series
RequestSize 设置允许接收最大请求包的大小。如果请求包大小超过设置的值,则返回 413 Payload TooLarge 请求包大小,单位为字节,默认值为5M
ModifyRequestBody 在转发请求之前修改原始请求体内容 修改后的请求体内容
ModifyResponseBody 修改原始响应体的内容 修改后的响应体内容

常用的过滤器

4.5.1 路径过滤器 RewritePath

重写url,将 /api/product/test/1 重写为 product/test/1 直接访问 http://127.0.0.1:9000/api/product/test/1即可

  1. gateway:
  2. routes:
  3. - id: provider-product
  4. uri: lb://provider-product
  5. predicates:
  6. - Path=/api/**
  7. filters:
  8. - RewritePath=/api(?<segment>/?.*), $\{segment}

4.5.2 路径过滤器 PrefixPath

直接访问 http://127.0.0.1:9000/test/1 可以在目标微服务前添加product 变成 /product/test/1

  1. gateway:
  2. routes:
  3. - id: provider-product
  4. uri: lb://provider-product
  5. predicates:
  6. - Path=/**
  7. filters:
  8. - PrefixPath=/product

4.5.3 路径过滤器 StripPrefix

去掉几层前缀就写多少 ,访问 http://127.0.0.1:9000/abc/product/test/1 去掉abc 这层前缀变成 /product/test/1

  1. gateway:
  2. routes:
  3. - id: provider-product
  4. uri: lb://provider-product
  5. predicates:
  6. - Path=/abc/**
  7. filters:
  8. - StripPrefix=1

4.5.4 路径过滤器 SetPath

重新修改url /abc/product/test/1 修改为 /product/test/1

  1. gateway:
  2. routes:
  3. - id: provider-product
  4. uri: lb://provider-product
  5. predicates:
  6. - Path=/abc/product/test/{segment}
  7. filters:
  8. - SetPath=/product/test/{segment}

4.5.5 参数过滤器 Parameter 参数过滤器

在请求的时候添加参数,就能自动带入到下个接口中去了

  1. gateway:
  2. routes:
  3. - id: provider-product
  4. uri: lb://provider-product
  5. predicates:
  6. - Path=/api/**
  7. filters:
  8. - StripPrefix=1
  9. - AddRequestParameter=flag, 256

image.png

4.5.6 参数过滤器 Parameter 参数过滤器

正常访问 http://127.0.0.1:9000/api/product/test/2 接口 页面的HTTP Code就是405了

  1. gateway:
  2. routes:
  3. - id: provider-product
  4. uri: lb://provider-product
  5. predicates:
  6. - Path=/api/**
  7. filters:
  8. - StripPrefix=1
  9. - SetStatus=405

4.5.7 自定义网关过滤器

自定义日志过滤器 继承 AbstractGatewayFilterFactory

  1. package com.rem.cloudalibaba.filter;
  2. import lombok.Data;
  3. import org.springframework.cloud.gateway.filter.GatewayFilter;
  4. import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
  5. import org.springframework.stereotype.Component;
  6. import java.util.Arrays;
  7. import java.util.List;
  8. /**
  9. * 自定义日志过滤器
  10. *
  11. * @author Rem
  12. * @date 2021-09-22
  13. */
  14. @Component
  15. public class LogGatewayFilterFactory extends AbstractGatewayFilterFactory<LogGatewayFilterFactory.Config> {
  16. /**
  17. * 构造函数
  18. */
  19. public LogGatewayFilterFactory() {
  20. super(LogGatewayFilterFactory.Config.class);
  21. }
  22. /**
  23. * 读取配置文件的参数,赋值到配置类中
  24. *
  25. * @return
  26. */
  27. @Override
  28. public List<String> shortcutFieldOrder() {
  29. return Arrays.asList("consoleLog", "cacheLog");
  30. }
  31. /**
  32. * 过滤器逻辑
  33. *
  34. * @param config
  35. * @return
  36. */
  37. @Override
  38. public GatewayFilter apply(LogGatewayFilterFactory.Config config) {
  39. return (exchange, chain) -> {
  40. if (config.isCacheLog()) {
  41. System.out.println("cacheLog 已经开启了");
  42. }
  43. if (config.isConsoleLog()) {
  44. System.out.println("console 已经开启了");
  45. }
  46. return chain.filter(exchange);
  47. };
  48. }
  49. @Data
  50. public static class Config {
  51. private boolean consoleLog;
  52. private boolean cacheLog;
  53. }
  54. }
  1. gateway:
  2. routes:
  3. - id: provider-product
  4. uri: lb://provider-product
  5. predicates:
  6. - Path=/api/**
  7. filters:
  8. - StripPrefix=1
  9. - Log=true, false

4.5.8 自定义全局过滤器

可以自定义实现自己的逻辑
访问 http://127.0.0.1:9000/api/product/test/1?**token=remTok **必须带Token 不然就鉴权失败

  1. package com.rem.cloudalibaba.filter;
  2. import com.alibaba.nacos.common.utils.StringUtils;
  3. import org.springframework.cloud.gateway.filter.GatewayFilterChain;
  4. import org.springframework.cloud.gateway.filter.GlobalFilter;
  5. import org.springframework.core.Ordered;
  6. import org.springframework.http.HttpStatus;
  7. import org.springframework.http.server.reactive.ServerHttpResponse;
  8. import org.springframework.stereotype.Component;
  9. import org.springframework.web.server.ServerWebExchange;
  10. import reactor.core.publisher.Mono;
  11. /**
  12. * 全局过滤器
  13. *
  14. * @author Rem
  15. * @date 2021-09-24
  16. */
  17. @Component
  18. public class AuthGlobalFilter implements GlobalFilter, Ordered {
  19. /**
  20. * 完成判断逻辑
  21. *
  22. * @param exchange
  23. * @param chain
  24. * @return
  25. */
  26. @Override
  27. public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
  28. String token = exchange.getRequest().getQueryParams().getFirst("token");
  29. if (StringUtils.isBlank(token) || !"remTok".equals(token)) {
  30. System.out.println("鉴权失败");
  31. ServerHttpResponse response = exchange.getResponse();
  32. response.setStatusCode(HttpStatus.UNAUTHORIZED);
  33. return response.setComplete();
  34. }
  35. //调用chain.filter继续向下游执行
  36. return chain.filter(exchange);
  37. }
  38. /**
  39. * 顺序,数值越小,优先级越高
  40. *
  41. * @return
  42. */
  43. @Override
  44. public int getOrder() {
  45. return 0;
  46. }
  47. }

4.6 限流

4.6.1 计数器算法

以QPS(每秒查询率Queries-per-second)为100举例。从第一个请求开始计时。每个请求让计数器加一。当到达100以后,其他的请求都拒绝。
如果1秒钟内前200ms请求数量已经到达了100,后面800ms中500次请求都被拒绝了,这种情况称为“突刺现象”

4.6.2 限流算法

漏桶算法可以解决突刺现象。
和生活中漏桶一样,有一个水桶,下面有一个”漏眼”往出漏水,不管桶里有多少水,漏水的速率都是一样的。但是既然是一个桶,桶里装的水都是有上限的。当到达了上限新进来的水就装不了(主要出现在突然倒进来大量水的情况)。
1.png

4.6.3 令牌桶算法

令牌桶算法可以说是对漏桶算法的一种改进。
在桶中放令牌,请求获取令牌后才能继续执行。如果桶中没有令牌,请求可以选择进行等待或者直接拒绝。由于桶中令牌是按照一定速率放置的,所以可以一定程度解决突发访问。如果桶中令牌最多有100个,qps最大为100
2.png

4.6.3.1 基于令牌桶算法的URL限流

新增redis的poom 依赖

  1. package com.rem.cloudalibaba.config;
  2. import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
  3. import org.springframework.context.annotation.Bean;
  4. import org.springframework.context.annotation.Configuration;
  5. import reactor.core.publisher.Mono;
  6. /**
  7. * @author Rem
  8. * @date 2021-10-20
  9. */
  10. @Configuration
  11. public class KeyResolveConfiguration {
  12. @Bean
  13. KeyResolver pathKeyResolver() {
  14. return exchange -> Mono.just(exchange.getRequest().getURI().getPath());
  15. }
  16. }
  1. spring:
  2. application:
  3. name: api-gateway
  4. cloud:
  5. nacos:
  6. discovery:
  7. namespace: 3d0a77b8-817f-499b-bfda-f90d5a6e4dab
  8. server-addr: 192.168.19.128:13306
  9. group: DEFAULT_GROUP
  10. username: nacos
  11. password: nacos
  12. gateway:
  13. routes:
  14. - id: provider-product
  15. uri: lb://provider-product
  16. predicates:
  17. - Path=/api/**
  18. filters:
  19. - StripPrefix=1
  20. # 限流过滤器
  21. - name: RequestRateLimiter
  22. args:
  23. redis-rate-limiter.replenishRate: 1 # 令牌桶每秒生成率
  24. redis-rate-limiter.burstCapacity: 5 #令牌桶总容量 每秒最大请求量
  25. redis-rate-limiter.requestedTokens: 1 # 每次请求消耗的令牌
  26. key-resolver: "#{@pathKeyResolver}"
  27. redis:
  28. host: 192.168.19.128

当访问频率大于每秒5次并且还大于令牌桶生成令牌的速率, 就会报HHTP Code 429

4.6.3.2 基于令牌桶算法的参数限流

  1. @Bean
  2. KeyResolver userKeyResolver() {
  3. return exchange -> Mono.just(Objects.requireNonNull(exchange.getRequest().getQueryParams().getFirst("userId")));
  4. }

4.6.3.3 基于令牌桶算法的IP限流

  1. @Bean
  2. KeyResolver ipKeyResolver() {
  3. return exchange -> Mono.just(Objects.requireNonNull(Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getHostName()));
  4. }

基于令牌的限流 KeyResolver在一个项目中只能存在一个 所以只能选择一种限流方式。

4.6.4 gateway整合 sentinel

配置后就可以直接通过sentinel 对接口限流

  1. server:
  2. port: 9000
  3. spring:
  4. application:
  5. name: api-gateway
  6. cloud:
  7. nacos:
  8. discovery:
  9. namespace: 3d0a77b8-817f-499b-bfda-f90d5a6e4dab
  10. server-addr: 192.168.19.128:13306
  11. group: DEFAULT_GROUP
  12. username: nacos
  13. password: nacos
  14. sentinel:
  15. # 服务启动直接建立心跳连接
  16. eager: true
  17. transport:
  18. dashboard: 127.0.0.1:8080
  19. port: 8719 #默认为8719 ,假如被占用会自动从8719开始依次+1扫描,直到未被占用的端口
  20. datasource:
  21. flow:
  22. nacos:
  23. server-addr: ${spring.cloud.nacos.discovery.server-addr}
  24. namespace: ${spring.cloud.nacos.discovery.namespace}
  25. groupId: SENTINEL_GROUP
  26. username: ${spring.cloud.nacos.discovery.username}
  27. password: ${spring.cloud.nacos.discovery.password}
  28. dataId: ${spring.application.name}-flow-rules
  29. rule-type: flow
  30. gateway:
  31. routes:
  32. - id: provider-product
  33. uri: lb://provider-product
  34. predicates:
  35. - Path=/api/**
  36. filters:
  37. - StripPrefix=1
  38. # 限流过滤器
  39. - name: RequestRateLimiter
  40. args:
  41. redis-rate-limiter.replenishRate: 1 # 令牌桶每秒生成率
  42. redis-rate-limiter.burstCapacity: 5 #令牌桶总容量 每秒最大请求量
  43. redis-rate-limiter.requestedTokens: 1 # 每次请求消耗的令牌
  44. # key-resolver: "#{@userKeyResolver}"
  45. redis:
  46. host: 192.168.19.128

image.png
image.png