SpringCloud05:Gateway

Gateway的功能

  1. 统一入口:是所有业务集群请求的入口。前端不需要记住所有微服务的地址,只需要记住网关的地址,由网关统一分配调用哪个微服务。
  2. 请求路由:网关会根据请求的路径,比如xxx/order/yyyy,去注册中心找到微服务的地址,然后把请求转发的对应微服务。
  3. 负载均衡:网关同时可以根据负载均衡算法合理的给每一个服务器分发请求。
  4. 流量控制:Sentinel可以融入到微服务当中,控制每一个微服务的QPS。而Gateway由于是流量入口,可以给全局的QPS进行统一的限流。
  5. 身份认证:比如,网关访问的内容需要登录,但是用户没有登录,就可以把请求转发到登录页。又比如,一些非法攻击请求,就可以把请求直接拒绝。
  6. 协议转换:比如,前端发的请求是一个Json数据, 但是后台要求的是gRPC协议。这个时候就可以通过网关进行协议转换。
  7. 系统监控:由于每一个请求都是从这里经过,那么就可以统计每一个请求从开始到结束的业务处理时间,从而统计全局的慢请求和访问总量等各种监控数据。
  8. 安全防护:从网关层面,我们可以配置一些防止跨站请求伪造、跨站脚本攻击和Sql注入等常见的安全问题

Gateway目前有两种网关:Reactive ServerServer MVC

  • Reactive Server:基于响应式编程做的网关。可以实现占用少量资源就实现高并发。(推荐)。
  • Server MVC:这是一个传统的网关。用的是severlet那一套。

需求

  1. 客户端发送 /api/order/** 转到 service-order

  2. 客户端发送 /api/product/** 转到 service-product

  3. 以上转发有负载均衡效果

创建网关

由于网关是架构层面的事情,而不是业务层面的。所以建议单独用一个模块(微服务)做网关。

引入gateway依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

由于需要请求转发,所以需要引入注册中心的依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

指定端口号和Nacos的地址

spring:
  application:
    name: gateway
  # 配置Nacos作为注册中心和配置中心,gateway也需要注册到Nacos
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848
# 写80端口,这样访问的时候可以直接使用localhost/api/order,而不需要指定端口
server:
  port: 80

这个时候gateway成功注册到注册中心

因为网关需要找到其他微服务的地址,所有还需要开启服务发现:

@EnableDiscoveryClient // 开启服务注册发现
@SpringBootApplication
public class GatewayMainApplication {

    public static void main(String[] args) {
        SpringApplication.run(GatewayMainApplication.class, args);
    }
}

路由

规则配置

创建一个application-route.yml文件专门放置路由规则

spring:
  cloud:
    gateway:
      routes:
        - id: order-route  # 路由ID,就是给这个路由起个名字
          uri: lb://service-order # 使用负载均衡的方式访问service-order服务,lb表达load balancer 负载均衡
          predicates: # 路由的断言,表示当请求满足以下条件时,才会匹配到这个路由
            # 表示当请求的路径以/api/order/开头时,才会匹配到这个路由
            # **表示匹配任意字符,包括斜杠,意思是/api/order/后面可以跟任意路径,比如/api/order/123,/api/order/abc/def等
            - Path=/api/order/** 
        - id: product-route
          uri: lb://service-product
          predicates:
            - Path=/api/product/**

在主配置文件里面引入application-route.yml

spring:
  profiles:
    # 引入路由配置文件,路由配置文件的路径为application-route.yml
    # 为什么填route 而不是application-route.yml? 这是因为Spring Boot会自动加载application-<profile>.yml文件
    include: route 
  application:
    name: gateway
  # 配置Nacos作为注册中心和配置中心,gateway也需要注册到Nacos
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848

由于我们在配置文件里面配置了负载均衡,所以还需要引入负载均衡的依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

否则会报503的错误

工作原理

路由的顺序

spring:
  cloud:
    gateway:
      routes:
        - id: bing-route
          uri: https://cn.bing.com/
          predicates:
            - Path=/**
        - id: order-route  
          uri: lb://service-order 
          predicates: 
            - Path=/api/order/**
        - id: product-route
          uri: lb://service-product
          predicates:
            - Path=/api/product/**

路由的顺项默认是至上而下的。如上面的配置,由于第一个配置了Path=/**,那么所有的请求都会被拦截,然后其访问https://cn.bing.com/

但是,我们可以用order指定顺序,数字越小优先级越高

spring:
  cloud:
    gateway:
      routes:
        - id: bing-route
          uri: https://cn.bing.com/
          predicates:
            - Path=/**
          order: 100 # 路由的优先级,数字越小优先级越高
        - id: order-route  
          uri: lb://service-order 
          predicates: 
            - Path=/api/order/**
          order: 0
        - id: product-route
          uri: lb://service-product
          predicates:
            - Path=/api/product/**
          order: 1

断言

长短写法

短写法:- Path=/api/order/**Path是断言的名字。/api/order/**是断言的值。

全写法需要写下面两个参数:nameargs

public class PredicateDefinition {
    private @NotNull String name;
    private Map<String, String> args = new LinkedHashMap();

把短写法改成全写法的格式如下:

predicates: 
  - name: Path
    args:
        patterns: /api/order/**
        matchTrailingSlash: true # 是否匹配末尾的斜杠

matchTrailingSlash默认为true。如果为true,那么/api/order/1/api/order/1/就是等价的。如果为False,那么那两个路径就不是等价的。

为什么这里的namePath。这是因为SpringCloud定义了一个PathRoutePredicateFactory断言工厂。所以name: Path其实在选择哪些断言工厂。

public class PathRoutePredicateFactory extends AbstractRoutePredicateFactory<Config> {
    private static final Log log = LogFactory.getLog(PathRoutePredicateFactory.class);
    private static final String MATCH_TRAILING_SLASH = "matchTrailingSlash";
    private PathPatternParser pathPatternParser = new PathPatternParser();

    public PathRoutePredicateFactory() {
        super(Config.class);
    }

Gateway一共提供了如此多的断言工厂可供选择:

args选择什么取决于Config.class这个类。在这里配置里面定义了两个参数:patternsmatchTrailingSlash

@Validated
public static class Config {
    private List<String> patterns = new ArrayList();
    private boolean matchTrailingSlash = true;

    public Config() {
    }

断言规则

Query

spring:
  cloud:
    gateway:
      routes:
        - id: bing-route
          uri: https://cn.bing.com/
          predicates:
            - name: Path
              args:
                patterns: /search
            - name: Query
              args:
                param: q # 请求参数名
                regexp: haha # regex表示要填入正则表达式

上面的断言就必须满足两个条件,才会把请求发送到https://cn.bing.com/

  • 请求路径必须是/search
  • 必须带有请求参数:q=haha

所有最后的请求路径是:

自定义断言工厂

/**
 * 自定义路由断言工厂
 * 1. 继承 AbstractRoutePredicateFactory,并且知道泛型参数为 Config(Config为内部类)
 * 2. 定义配置类 Config,继承 AbstractRoutePredicateFactory 的 Config
 * 3. 构造无参构造方法,调用父类构造方法,并传入 Config.class
 * 4. 实现 apply 方法,返回一个 Predicate<ServerWebExchange>
 * 5. 实现 shortcutFieldOrder 方法,返回配置参数的顺序
 * 6. @Component 注解将该工厂注册到 Spring 容器中
 */
// 6. @Component 注解将该工厂注册到 Spring 容器中
@Component
// 1. 继承 AbstractRoutePredicateFactory
public class VipRoutePredicateFactory extends AbstractRoutePredicateFactory<VipRoutePredicateFactory.Config> {

    // 3. 构造无参构造方法,调用父类构造方法,并传入 Config.class
    public VipRoutePredicateFactory() {
        super(Config.class);
    }

    // 4. 实现 apply 方法,返回一个 Predicate<ServerWebExchange>
    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
        // new GatewayPredicate() 是一个函数式接口,表示一个断言
        // public interface GatewayPredicate extends Predicate<ServerWebExchange>, HasConfig {
        return new GatewayPredicate() {
            // serverWebExchange 是一个包含请求和响应的上下文对象
            @Override
            public boolean test(ServerWebExchange serverWebExchange) {
                // 获取请求对象
                ServerHttpRequest request = serverWebExchange.getRequest();
                // request.getQueryParams(): 获取请求的查询参数
                // request.getQueryParams().getFirst(config.param): 获取指定参数的第一个值
                // config.param: 从配置中获取参数名
                String first = request.getQueryParams().getFirst(config.param);
                // StringUtils.hasText(first): 检查 first 是否有文本内容
                // first.equals(config.value): 检查 first 是否等于配置中的值
                return StringUtils.hasText(first) && first.equals(config.value);
            }
        };
    }

    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList("param", "value");
    }

    /**
     * 2. 定义配置类 Config,继承 AbstractRoutePredicateFactory 的 Config
     */
    @Validated
    public static class Config {
        // Config里面为vip路由断言工厂的配置参数。有两个参数
        @NotEmpty
        private String param;


        @NotEmpty
        private String value;

        public @NotEmpty String getParam() {
            return param;
        }

        public void setParam(@NotEmpty String param) {
            this.param = param;
        }

        public @NotEmpty String getValue() {
            return value;
        }

        public void setValue(@NotEmpty String value) {
            this.value = value;
        }
    }
}

在配置文件里面作如下配置:

spring:
  cloud:
    gateway:
      routes:
        - id: bing-route
          uri: https://cn.bing.com/
          predicates:
            - name: Path
              args:
                patterns: /search
            - name: Query
              args:
                param: q # 请求参数名
                regexp: haha # regex表示要填入正则表达式
            - name: Vip # 表示VIP路由,只有当请求的VIP参数值为super时才会匹配到这个路由
              args:
                param: user # VIP参数名
                value: super # VIP参数值

过滤器

基本原理

一个请求会经过多个过滤器的前置方法,然后才能到达目的地(所指定的url路径)。

其响应也会经过多个过滤器的后置方法,最后返回给用户。

在配置中,也只是指定过滤器的过滤器工厂。

路径重写过滤器

需求:发送请求的路径是/api/order/readDb,经过路由之后变成/readDb

spring:
  cloud:
    gateway:
      routes:
        - id: order-route  
          uri: lb://service-order 
          predicates: 
            - name: Path
              args:
                  patterns: /api/order/**
                  matchTrailingSlash: true 
          filters: # 路由的过滤器,表示对请求和响应进行处理
            # 重写请求路径,将/api/order/后面的部分提取出来,作为新的请求路径
            # ?(?<segment>.*): 表示匹配/api/order/后面的任意字符,并将其提取到segment变量中
            #  ?(?<segment>.*) 中的?表示可选,.*表示匹配任意字符,()表示分组,就是将匹配到的部分提取出来
            # /$(\{segment}): 表示将segment变量的值作为新的请求路径
            # 比如/api/order/123会被重写为/123,/api/order/abc/def会被重写为/abc/def
            - RewritePath=/api/order/?(?<segment>.*), /$\{segment}

添加响应头的过滤器

spring:
  cloud:
    gateway:
      routes:
        - id: order-route  
          uri: lb://service-order 
          predicates: 
            - name: Path
              args:
                  patterns: /api/order/**
                  matchTrailingSlash: true 
          filters: 
            - RewritePath=/api/order/?(?<segment>.*), /$\{segment}
            - AddResponseHeader=abc, 123 # 添加响应头,表示在响应中添加一个名为abc的头,值为123

添加默认过滤器

spring:
  cloud:
    gateway:
      routes:
        - id: product-route
          uri: lb://service-product
          predicates:
            - Path=/api/product/**
          filters:
            - RewritePath=/api/product/?(?<segment>.*), /$\{segment}
          order: 1
      default-filters: # 默认过滤器,表示对所有路由都生效
        - AddResponseHeader=Abc, 123 # 添加响应头,表示在响应中添加一个名为X-Response-Abc的头,值为123

GlobalFilter

/**
 * 全局过滤器
 * 1. 实现 GlobalFilter 接口
 * 2. 实现 Ordered 接口,指定过滤器的执行顺序
 * 3. 在 filter 方法中编写前置和后置逻辑
 * 
 * GlobalFilter全局过滤器的作用是对所有请求进行统一处理。
 */

@Component
@Slf4j
public class RtGlobalFilter implements GlobalFilter, Ordered {
    /**
     * 全局过滤器的作用是对所有请求进行统一处理。
     * @param exchange : ServerWebExchange 是 Spring WebFlux 中的一个接口,表示一个服务器端的 Web 交换上下文。
     *                 它包含了请求和响应的相关信息,可以用于处理 HTTP 请求和响应。
     * @param chain: GatewayFilterChain 是 Spring Cloud Gateway 中的一个接口,表示过滤器链。
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // exchange.getRequest(): 获取当前请求的 ServerHttpRequest 对象
        ServerHttpRequest request = exchange.getRequest();
        // exchange.getResponse(): 获取当前响应的 ServerHttpResponse 对象
        ServerHttpResponse response = exchange.getResponse();

        // request.getURI(): 获取请求的 URI。URI(Uniform Resource Identifier)是一个统一资源标识符,用于唯一标识一个资源。
        String uri = request.getURI().toString();
        long start = System.currentTimeMillis();
        log.info("请求【{}】开始:时间:{}",uri,start);
        //========================以上是前置逻辑=========================

        // chain.filter(exchange):这是一个 Mono<Void> 对象,表示对请求进行处理的异步操作。
        // filter 方法会将请求传递给下一个过滤器或处理器,并返回一个 Mono<Void> 对象。
        // Mono 是 Reactor 中的一个响应式类型,表示一个异步操作的结果。
        Mono<Void> filter = chain.filter(exchange) // 放行
                // .doFinally 是一个 Reactor 中的操作符,用于在 Mono 或 Flux 完成时执行一些操作。也就是最后的后置逻辑。
                .doFinally((result)->{
                    //=======================以下是后置逻辑=========================
                    long end = System.currentTimeMillis();
                    log.info("请求【{}】结束:时间:{},耗时:{}ms",uri,end,end-start);
                }); 
        // 由于是异步操作,所有下面的代码不会阻塞当前线程,也就不能说后置逻辑

        return filter;
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

由于使用了lombok.extern.slf4j.Slf4j,还需要引入lombok

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <!--   `<scope>annotationProcessor</scope>` 表示该依赖是用于注解处理器的。
    注解处理器(Annotation Processor)是一种在编译时处理注解的工具,通常用于生成代码或执行其他编译时任务。
    在你的代码中,`lombok` 依赖被标记为 `annotationProcessor`,
        这意味着它的作用是处理 `@Getter`、`@Setter`、`@Builder` 等 Lombok 提供的注解,
        并在编译时生成相应的代码(如 getter/setter 方法)。这种方式可以减少手动编写样板代码的工作量。
    将 `lombok` 的作用域设置为 `annotationProcessor`,可以确保它只在编译时生效,
        而不会被打包到最终的应用程序中,从而减少运行时的依赖。 表示         -->
    <scope>annotationProcessor</scope>
</dependency>

常见过滤器

过滤器名称 作用 参数(含义) 示例(YAML 配置)
AddRequestHeader 向请求头中添加一个 header name: header名value: header值 AddRequestHeader=X-Request-Color, blue
AddRequestParameter 向请求参数中添加一个 query param name: 参数名value: 参数值 AddRequestParameter=lang, zh
AddResponseHeader 向响应头中添加一个 header name: header名value: header值 AddResponseHeader=X-Resp-Time, 123
RewritePath 重写请求路径(URL 路径重写) regexp: 匹配路径replacement: 替换内容 RewritePath=/foo/(?<segment>.*), /${segment}
PrefixPath 给路径前面加前缀 prefix: 前缀路径 PrefixPath=/api
StripPrefix 删除请求路径前面的部分 parts: 删除的路径段数量(int) StripPrefix=1
RedirectTo 重定向到指定 URL status: 状态码(302/301)url: 新地址 RedirectTo=302, https://example.com
SetPath 设置请求的路径(完全替换) path: 新路径 SetPath=/new-path
SetStatus 设置响应状态码 status: 状态码(如 401, 403 SetStatus=401
RemoveRequestHeader 移除请求头 name: header名 RemoveRequestHeader=Authorization
RemoveResponseHeader 移除响应头 name: header名 RemoveResponseHeader=X-Powered-By
SetRequestHeader 设置请求头(如果已存在则覆盖) name: header名value: header值 SetRequestHeader=token, my-token
SetResponseHeader 设置响应头(如果已存在则覆盖) name: header名value: header值 SetResponseHeader=Server, MyGateway
RequestRateLimiter 限流(结合 Redis 使用) redis-rate-limiter.replenishRate: 令牌恢复速率burstCapacity: 突发容量key-resolver: 限流键解析器 RequestRateLimiter={"replenishRate":1,"burstCapacity":2,"key-resolver": "#{@ipKeyResolver}"}
Retry 请求重试 retries: 重试次数statuses: 哪些状态码触发methods: 哪些方法触发 Retry=3, GET, 500
CircuitBreaker 熔断器(结合 Resilience4j 使用) name: 熔断器名fallbackUri: 熔断后的降级跳转地址 CircuitBreaker={"name":"myCB","fallbackUri":"forward:/fallback"}
RequestSize(Spring Cloud 2022+) 限制请求体最大字节数 maxSize: 最大字节数(long) RequestSize=1048576

自定义过滤器

/**
 * 自定义过滤器:一次性令牌过滤器工厂
 * 实现的效果:不同的请求响应中添加一个一次性令牌,
 *          如果配置为 uuid,则添加一个随机的 UUID;
 *          如果配置为 jwt,则添加一个固定的 JWT。
 * OnceTokenGatewayFilterFactory:filter名字+GatewayFilterFactory
 * 1. 继承 AbstractNameValueGatewayFilterFactory
 * 2. 实现 apply 方法,返回一个 GatewayFilter
 * 3. 在 filter 方法中添加一次性令牌到响应头
 * 4. @Component 注解将该工厂注册到 Spring 容器中
 */

@Component
public class OnceTokenGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
    /**
     * apply 方法是用来创建 GatewayFilter 的方法。
     * @param config : NameValueConfig 是一个配置类,包含了名称和值。这个config是在配置文件中定义的。
     * @return
     */
    @Override
    public GatewayFilter apply(NameValueConfig config) {
        return new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
              
                // .then : 在 Mono 完成后执行一个操作。Mono 完成后执行的意思是: 这里的代码是在响应生成之后执行的
                return chain.filter(exchange).then(Mono.fromRunnable(()->{
                    ServerHttpResponse response = exchange.getResponse();
                    HttpHeaders headers = response.getHeaders();
                    String value = config.getValue();
                    if ("uuid".equalsIgnoreCase(value)){
                        value = UUID.randomUUID().toString();
                    }

                    if ("jwt".equalsIgnoreCase(value)){
                        value = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ";
                    }

                    headers.add(config.getName(),value);
                }));
            }
        };
    }
}
代码片段 含义
chain.filter(exchange) 继续执行后续过滤器链和最终路由请求(这是异步的)
.then(...) 等上面的 Mono<Void> 执行完成后,再执行 Mono.fromRunnable(...) 中的逻辑
Mono.fromRunnable(...) 创建一个空值 Mono,执行其中的 Runnable(无返回值)逻辑
exchange.getResponse() 获取响应对象
config.getValue() 从配置中获取你传入的参数值(如 uuidjwt
headers.add(config.getName(), value) 向响应头添加一个自定义 Header,Header 的 key 来自 config.getName(),value 是你生成的 UUID 或 JWT

.then().doFinally()的区别

场景 推荐使用
你只在请求/响应成功后做事(如添加响应头) .then(...)
你无论结果如何都要做事(如日志、资源释放) .doFinally(...)

.then(...) 方法的原型(简化):

public final <V> Mono<V> then(Publisher<V> other)

.then(...) 接收的是任何 Publisher<V> 类型的对象,通常是 Mono<T>Flux<T>,并不是限定只能用 Runnable

类型 说明
Mono.fromRunnable(() -> {...}) 用于无返回值的副作用操作
Mono.just(value) 用于有固定返回值的逻辑
任何 Mono<T> 都可以作为 .then() 的参数
.then() 不返回前一个 Mono 的结果,只返回你传入的那个 Mono 的结果

使用:

spring:
  cloud:
    gateway:
      routes:
        - id: order-route  
          uri: lb://service-order 
          predicates: 
            - name: Path
              args:
                  patterns: /api/order/**
                  matchTrailingSlash: true 
          filters: 
            - RewritePath=/api/order/?(?<segment>.*), /$\{segment}
            # OnceToken表示只处理一次令牌,X-Response-Token是响应头的名称,jwt是令牌的类型
            - OnceToken=X-Response-Token, jwt
          order: 0

跨域Cors

全局的跨域配置:

spring:
  cloud:
    gateway:
      globalcors: # globalcors表示全局CORS配置
        cors-configurations: #cors-configurations表示CORS配置
          '[/**]': # 表示对所有路径都生效
            allowed-origin-patterns: '*' # 允许所有来源的请求
            allowed-headers: '*' # 允许所有请求头。
            allowed-methods: '*' # 允许所有HTTP方法,包括GET、POST、PUT、DELETE等

配置好之后响应头里面会多出这些设置:

**局部的跨域配置:**针对某个routes

spring:
  cloud:
    gateway:
      routes:
      - id: cors_route
        uri: https://example.org
        predicates:
        - Path=/service/**
        metadata:
          cors:
            allowedOrigins: '*'
            allowedMethods:
              - GET
              - POST
            allowedHeaders: '*'
            maxAge: 30

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1909773034@qq.com

×

喜欢就点赞,疼爱就打赏