微服务网关Spring Cloud Gateway

一、微服务网关

我们的各种微服务是通过REST API接口提供给外部应用调用,而客户端直接与各个微服务通讯则会出现一些问题:

  • 客户端会多次请求不同的微服务,增加了客户端的复杂性。
  • 存在跨域请求,在一定场景下处理会变得相对比较复杂。
  • 实现认证复杂,每个微服务都需要独立认证。
  • 难以重构,项目迭代可能导致微服务重新划分。如果客户端直接与微服务通讯,那么重构将会很难实施。
  • 如果某些微服务使用了防火墙、浏览器不友好的协议,直接访问会有一定困难。

而我们就是要通过微服务网关解决这些问题,网关的用处有许多方面:

  • 网关可以做一些身份认证、权限管理、防止非法请求操作服务等,对服务起一定保护作用。
  • 网关将所有微服务统一管理,对外统一暴露,外界系统不需要知道微服务架构个服务相互调用的复杂性,同时也避免了内部服务一些敏感信息泄露问题。
  • 易于监控。可在微服务网关收集监控数据并将其推送到外部系统进行分析。
  • 客户端只跟服务网关打交道,减少了客户端与各个微服务之间的交互次数。
  • 多渠道支持,可以根据不同客户端(WEB端、移动端、桌面端...)提供不同的API服务网关。
  • 网关可以用来做流量监控。在高并发下,对服务限流、降级。
  • 网关把服务从内部分离出来,方便测试。

        在微服务架构中,后端服务往往不直接开放给调用端,而是通过一个API网关根据请求的URL,路由到相应的服务。当添加API网关后,在第三方调用端和服务提供方之间就创建了一面墙,在API网关中进行权限控制,同时API网关将请求以负载均衡的方式发送给后端服务。

微服务网关架构:

微服务网关

二、Spring Cloud Gateway

简介

        Spring Cloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。

        SpringCloud Gateway作为Spring Cloud 生态系统中的网关,是基于WebFlux框架是实现的,而WebFlux框架是使用高性能的Reactor模式通信框架Netty。Spring Cloud Gateway 的目标,不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。

特征 

  • 基于 Spring Framework 5、Project Reactor 和 Spring Boot 2.0 构建

  • 能够匹配任何请求属性的路由。

  • 谓词和过滤器特定于路由。

  • 断路器集成。

  • Spring Cloud Discovery客户端集成

  • 易于编写谓词和过滤器

  • 请求速率限制

  • 路径重写

比较重要的三个概念:过滤器(Filter)、路由、断言(易于编写谓词

(1)Filter(过滤器):

和Zuul的过滤器在概念上类似,可以使用它拦截和修改请求,并且对下游的响应,进行二次处理。过滤器为org.springframework.cloud.gateway.filter.GatewayFilter类的实例。

(2)Route(路由):

网关配置的基本组成模块,和Zuul的路由配置模块类似。一个Route模块由一个 ID,一个目标 URI,一组断言和一组过滤器定义。如果断言为真,则路由匹配,目标URI会被访问。

(3)Predicate(断言):

这是一个 Java 8 的 Predicate,可以使用它来匹配来自 HTTP 请求的任何内容,例如 headers 或参数。断言的输入类型是一个 ServerWebExchange。

处理流程

        客户端向 Spring Cloud Gateway 发出请求。然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(“pre”)或之后(“post”)执行业务逻辑。

Spring Cloud Gateway处理流程

路由配置方式

        路由是网关配置的基本组成模块,和Zuul的路由配置模块类似。一个Route模块由一个 ID,一个目标 URI,一组断言和一组过滤器定义。如果断言为真,则路由匹配,目标URI会被访问。

基础路由配置方式

spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:
        - id: service1
          uri: https://blog.csdn.net
          predicates:
            - Path=/csdn

含义:该网关微服务的名称gateway,配置了一个id为service1的URI代理规则,这个路由规则是:当我们访问地址为 域名:端口号/csdn/xx.jsp时,会路由到上游地址https://blog.csdn.net/xx.jsp。

  • id:我们自定义的路由 ID,保持唯一
  • uri:目标服务地址
  • predicates:路由条件,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。

和注册中心相结合的路由配置方式

基于代码的路由配置方式:我们在启动类GateWayApplication中添加方法,用于定制转发规则:

@Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
                .route("path_route", r -> r.path("/csdn")
                        .uri("https://blog.csdn.net"))
                .build();
    }

不过这种方式不常使用。

        在uri的schema协议部分为自定义的lb:类型,表示从微服务注册中心(如Eureka)订阅服务,并且通过负载均衡进行服务的路由。

server:
  port: 9005
spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes:
        - id: service1
          uri: https://blog.csdn.net
          predicates:
            - Path=/csdn
        - id: service2
#          uri: http://127.0.0.1:9001
          uri: lb://cloud-payment-service
          predicates:
            - Path=/payment/**
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:9004/eureka

        注册中心相结合的路由配置方式,与单个URI的路由配置,区别其实很小,仅仅在于URI的schema协议不同。单个URI的地址的schema协议,一般为http或者https协议。启动多个支付微服务,可以会发现我们配置的相关端口会轮流出现。

路由匹配规则

Spring Cloud Gateway的主要功能之一是转发请求,转发规则的定义主要包含三个部分:

转发规则

        Spring Cloud Gateway 是通过 Spring WebFlux 的 HandlerMapping 做为底层支持来匹配到转发路由,Spring Cloud Gateway 内置了很多 Predicates 工厂,这些 Predicates 工厂通过不同的 HTTP 请求参数来匹配,多个 Predicates 工厂可以组合使用。

路由匹配规则

Predicate 断言条件

        Predicate 来源于 Java 8,是 Java 8 中引入的一个函数,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。可以用于接口请求参数校验、判断新老数据是否有变化需要进行更新操作。

断言条件

        Predicate 就是为了实现一组匹配规则,方便让请求过来找到对应的 Route 进行处理,接下来我们接下 Spring Cloud GateWay 内置几种 Predicate 的使用。

 举个例子:

spring:
  cloud:
    gateway:
      routes:    
        - id: service
          uri: lb://cloud-payment-service
          predicates:
            - Query=num,1
            - Header=X-Request-Id, \d+
            - Cookie=sessionName,Tom
            - Host=**.csdn.net
            - Method=POST
            - Path=/payment/{segment}

这里的组合Predicates条件含义:该路由配置的id为service,路由的uri地址表示使用负载均衡策略调用cloud-payment-service这个微服务,断言条件:访问路径中必须含有参数num,且值为1;请求头包含X-Request-Id,且参数这里使用的正则表达式表示任何整数值;cookie包含sessionName,且值为Tom;host主机地址需要以csdn.net结尾;请求方法为POST;请求路径/payment/{segment},表示/payment/后的一个子片段(可以作为一个变量)。

各种 Predicates 同时存在于同一个路由时,请求必须同时满足所有的条件才被这个路由匹配。一个请求满足多个路由的断言条件时,请求只会被首个成功匹配的路由转发。

过滤器规则

常见过滤器:

常见过滤器

 示例:

spring:
  application:
    name: api-gateway
# Spring Cloud Gateway 路由配置
  cloud:
    gateway:
      routes:
        # 过滤器规则
          # 1. PrefixPath 对所有的请求路径添加前缀
          - id: service10
            uri: http://127.0.0.1:9001
            predicates:
              - Path=/{segment}
            filters:
              - PrefixPath=/payment
          # 2. StripPrefix 跳过指定路径
          - id: service11
            uri: http://127.0.0.1:9001
            predicates:
              - Path=/api/{segment}
            filters:    # 这里是将第一个路径跳过,并且添加/payment/前缀
              - StripPrefix=1
              - PrefixPath=/payment
          # 3. RewritePath 重写路径
          - id: service12
            uri: http://127.0.0.1:9001
            predicates:
              - Path=/api/payment/**
            filters:
              - RewritePath=/api/(?<segment>.*), /$\{segment}   # 将前面的正则,改写为后面的正则(重写)
          # 4. SetPath SetPath和Rewrite类似
          - id: service13
            uri: http://127.0.0.1:9001
            predicates:
              - Path=/api/payment/{segment}
            filters:
              - SetPath=/payment/{segment}
          # 5. RemoveRequestHeader 去掉某个请求头信息
          - id: removerequestheader_route
            uri: https://example.org
            predicates:
              - Method=GET
            filters:
              - RemoveRequestHeader=X-Request-Foo
          # 6. RemoveResponseHeader 去掉某个回执头信息
          - id: removerequestheader_route
            uri: https://example.org
            predicates:
              - Method=GET
            filters:
              - RemoveResponseHeader=X-Request-Foo
          # 7. RemoveRequestParameter 去掉某个请求参数信息
          - id: removerequestparameter_route
            uri: https://example.org
            predicates:
              - Method=GET
            filters:
              - RemoveRequestParameter=red
#          # 8. SetRequestHeader 设置请求头信息
          - id: setrequestheader_route
            uri: https://example.org
            predicates:
              - Method=GET
            filters:
              - SetRequestHeader=X-Request-Red, Blue
          # 9. default-filters 对所有的请求添加过滤器
          - id: service13
            uri: http://127.0.0.1:9000
            predicates:
              - Path=/9000/{segment}
          - id: service14
            uri: http://127.0.0.1:9001
            predicates:
              - Path=/9001/{segement}
      default-filters:
        - StripPrefix=1
        - PrefixPath=/payment

自定义过滤器

        Spring-Cloud-Gateway 基于过滤器实现,同 zuul 类似,有pre和post两种方式的 filter,分别处理前置逻辑和后置逻辑。客户端的请求先经过pre类型的 filter,然后将请求转发到具体的业务服务,收到业务服务的响应之后,再经过post类型的 filter 处理,最后返回响应到客户端。

过滤器分为全局过滤器和局部过滤器。

  • 全局过滤器:对所有路由生效。
  • 局部过滤器:对指定的路由生效。

全局过滤器

全局过滤器需要实现GlobalFilter 和 Ordered两个接口,重写相关方法,加入到spring容器管理即可,无需配置,全局过滤器对所有的路由都有效。


/**
 * 全局过滤器
 * 实现 GlobalFilter 和 Ordered,重写相关方法,加入到spring容器管理即可,无需配置,全局过滤器对所有的路由都有效。
 */
@Configuration    //  注释后,该配置类不再生效
@Slf4j
public class FilterConfig {

    /**
     * 将自定义的全局过滤器,通过@Bean注册到spring容器中
     * @return
     */
    @Bean
    public GlobalFilter a() {
        return new AFilter();
    }
    @Bean
    public GlobalFilter b() {
        return new BFilter();
    }
    @Bean
    public GlobalFilter c() {
        return new CFilter();
    }
    @Bean
    public MyAuthFilter myAuthFilter() {
        return new MyAuthFilter();
    }

    static class AFilter implements GlobalFilter, Ordered {

        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            log.info("AFilter前置逻辑。。。");

            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                log.info("AFilter后置逻辑。。。");
            }));
        }

        @Override
        public int getOrder() {
            return HIGHEST_PRECEDENCE + 200;
        }
    }

    static class BFilter implements GlobalFilter, Ordered {

        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            log.info("BFilter前置逻辑。。。");

            //  chain.filter(exchange) 表示将exchange请求信息传递到下一个过滤器(执行过滤器链)
            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                log.info("BFilter后置逻辑。。。");
            }));
        }

        @Override
        public int getOrder() {
            return HIGHEST_PRECEDENCE + 100;
        }
    }

    static class CFilter implements GlobalFilter, Ordered {

        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            log.info("CFilter前置逻辑。。。");

            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                log.info("CFilter后置逻辑。。。");
            }));
        }

        @Override
        public int getOrder() {
            return HIGHEST_PRECEDENCE + 300;
        }
    }

    static class MyAuthFilter implements GlobalFilter, Ordered {

        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            log.info("MyAuthFilter过滤器执行");
            String token = exchange.getRequest().getHeaders().getFirst("token");
            if (StringUtils.isBlank(token)) {
                exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);  //  设置响应码为401
                return exchange.getResponse().setComplete();    //  结束此次请求
            }

            return chain.filter(exchange);
        }

        @Override
        public int getOrder() {
            return HIGHEST_PRECEDENCE + 400;
        }
    }
}

定义了4个全局过滤器,顺序为A>B>C>MyAuthFilter,其中全局过滤器MyAuthFilter中判断令牌是否存在,如果令牌不存在,则返回401状态码,表示没有权限访问。

局部过滤器

定义局部过滤器步骤如下。

(1)需要实现GatewayFilter, Ordered,实现相关的方法

(2)加入到过滤器工厂,并且注册到spring容器中。

(3)在配置文件中进行配置,如果不配置则不启用此过滤器规则。

举例:对于请求头user-id校验,如果不存在user-id请求头,直接返回状态码406。


/**
 * 局部过滤器
 * 定义类名需以 名称+ GatewayFilterFactory 格式,并且需继承AbstractGatewayFilterFactory<Object> 类
 */
@Component
public class UserIdCheckGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
    /**
     * 定义局部过滤器步骤如下。
     * (1)需要实现GatewayFilter, Ordered,实现相关的方法
     * (2)加入到过滤器工厂,并且注册到spring容器中。
     * (3)在配置文件中进行配置,如果不配置则不启用此过滤器规则。
     */

    @Override
    public GatewayFilter apply(Object config) {
        //  将自定义的局部过滤器返回
        return new userIdCheckGatewayFilter();
    }

    @Slf4j
    static class userIdCheckGatewayFilter implements GatewayFilter, Ordered {

        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            //  获取请求地址url
            String url = exchange.getRequest().getPath().pathWithinApplication().value();
            log.info("请求url:" + url);
            log.info("请求method: " + exchange.getRequest().getMethod());

            String userId = exchange.getRequest().getHeaders().getFirst("user-id");
            if (StringUtils.isBlank(userId)) {
                log.info("user-id请求头不能为空");
                exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);    //  设置状态码为406
                return exchange.getResponse().setComplete();   //  返回
            }

            return chain.filter(exchange);
        }

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

及yml配置文件中的配置如下:

spring:
  cloud:
    gateway:
      routes:        
        - id: service
          uri: http://127.0.0.1:9001
          predicates:
            - Path=/{segment}
      default-filters:
        - PrefixPath=/payment
        - UserIdCheck

统一跨域请求

跨域请求就是指:当前发起请求的域与该请求指向的资源所在的域不一样。这里的域指的是这样的一个概念:我们认为若协议 + 域名 + 端口号均相同,那么就是同域。

我们在某一个微服务下的资源目录创建一个index.html文件,用于ajax请求调用另一个微服务,但是由于浏览器同源资源策略,会出现跨域访问问题:CORS错误

        其中一个方法,在需要请求调用另一个微服务的类上添加注解 @CrossOrigin,那么就可以跨域访问该类,但是不推荐,如果需要跨域访问的资源比较多,我们需要在每个资源上添加这个注解。

        另一个方法,跨域配置:

现在请求经过gatway网关是,可以通过网关统一配置跨域访问。

spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]':
            allowed-origin-patterns: "*" # spring boot2.4配置
#            allowed-origins: "*"
            allowed-headers: "*"
            allow-credentials: true
            allowed-methods:
              - GET
              - POST
              - DELETE
              - PUT
              - OPTION

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值