SpringCloud下基于GateWay的服务网关实践

ProjectDaedalus

共 6254字,需浏览 13分钟

 · 2021-11-09

Spring Cloud GateWay 作为分布式架构下常见的服务网关,为内部各服务对外提供统一的API入口。同时还为对外提供服务的API提供统一的安全、鉴权、监控等功能

abstract.png

基本实践

目标服务

为了便于演示网关服务的作用,这里我们先提供一个目标服务——payment。其服务接口的Controller实现如下所示

@RestController
@RequestMapping("pay3")
public class PaymentController3 {

    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/test1")
    public String test1() {
        String uuid = UUID.randomUUID().toString();
        String msg = "[Payment Service - test1], port:" + serverPort
            + ", uuid: " + uuid;
        return msg;
    }

    @GetMapping("/test2")
    public String test2(@RequestParam String name) {
        String msg = "[Payment Service - test2], port:" + serverPort
            + ", name: " + name;
        return msg;
    }

    @GetMapping("/hello1")
    public String hello1() {
        String msg = "[Payment Service - hello1], port:" + serverPort;
        return msg;
    }

    @GetMapping("/hello2")
    public String hello2(@RequestParam Integer num) {
        String msg = "[Payment Service - hello2], port:" + serverPort
            + ", num: " + num;
        return msg;
    }

}

启动payment服务的两个实例,分别允许在8004、8005端口。这里我们使用Consul作为注册中心。如下所示

figure 1.jpeg

网关服务

现在我们建立一个ApiGateWay服务,用于实践我们的服务网关。首先在POM中引入 spring-cloud-starter-gateway 依赖

<dependencyManagement>
  <dependencies>

    
    <dependency>
      <groupId>org.springframework.bootgroupId>
      <artifactId>spring-boot-dependenciesartifactId>
      <version>2.2.2.RELEASEversion>
      <type>pomtype>
      <scope>importscope>
    dependency>

    
    <dependency>
      <groupId>org.springframework.cloudgroupId>
      <artifactId>spring-cloud-dependenciesartifactId>
      <version>Hoxton.SR1version>
      <type>pomtype>
      <scope>importscope>
    dependency>

  dependencies>
dependencyManagement>

<dependencies>

  
  <dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-starter-gatewayartifactId>
  dependency>

dependencies>

其配置文件如下所示。可以看到该服务网关也会被注册到Consul中。当然在ApiGateWay的POM中也需要引入Consul依赖。具体地,网关相关的配置即是通过spring.cloud.gateway配置项进行配置

server:
  port: 9527

spring:
  application:
    name: ApiGateWay
  cloud:
    # 注册中心Consul配置
    consul:
      # Consul 地址信息
      host: 127.0.0.1
      port: 8500
      discovery:
        # 服务名
        service-name: ${spring.application.name}
    # 网关GateWay配置
    gateway:
      discovery:
        locator:
          # 实现通过注册中心动态创建基于服务名的路由
          enabled: true
      routes:
          # 路由的唯一标识
        - id: payment_test1_route
          # 目标地址
          uri: http://localhost:8004
          # 路由匹配的谓词条件
          predicates:
            # Path谓词, 根据请求路径进行匹配
            - Path=/pay3/test1
        - id: payment_test2_route
          # 基于注册中心的动态路由, 格式: lb协议(lb://)+服务名
          uri: lb://payment
          predicates:
            - Path=/pay3/test2

其中:

  • spring.cloud.gateway.routes.id 配置项用于配置路由的唯一标识
  • spring.cloud.gateway.routes.uri 配置项用于路由匹配后进行转发的目标地址
  • spring.cloud.gateway.routes.predicates 配置项用于设置路由匹配所需的谓词条件。具体地,Path谓词用于根据路径进行路由匹配

具体对于payment_test1_route而言,其目标uri是具体地允许在某端口的服务。此举显然无法充分体现payment集群服务的作用。故可通过 spring.cloud.gateway.discovery.locator.enabled 配置项实现通过注册中心动态创建基于服务名的路由。这也是为什么需要在ApiGateWay服务添加Consul依赖。事实上,GateWay还支持通过Java配置类的方式进行路由配置,如下所示

@Configuration
public class GateWayConfig {

    @Bean
    public RouteLocator routeLocator1(RouteLocatorBuilder routeLocatorBuilder) {
        return routeLocatorBuilder.routes()
            .route("payment_hello1_route", r -> r.path("/pay3/hello1")
                .uri("http://localhost:8004") )
            .route("payment_hello2_route", r -> r.path("/pay3/hello2")
                .uri("lb://payment") )
            .build();
    }

}

至此ApiGateWay服务就已经基本完成了,其启动类如下所示

@SpringBootApplication
@EnableDiscoveryClient // 使用Consul作为注册中心时使用
public class ApiGateWayApplication {
    public static void main(String[] args) {
        SpringApplication.run(ApiGateWayApplication.classargs);
    }
}

启动ApiGateWay服务,测试结果如下,符合预期

figure 2.jpeg

Predicate谓词

根据前文可知,Predicate谓词即是GateWay进行路由转发时所需满足的匹配条件。这里对GateWay中常见的谓词进行介绍

After、Before、Between

After、Before、Between谓词要求请求时的时间分别位于所配置时间之后、之前、之间才满足匹配要求。配置示例如下所示

# Case 1
predicates:
  # 请求时间在指定时间之后才满足条件
  - After=2021-09-06T21:51:37.485+08:00[Asia/Shanghai]

# Case 2
predicates:
  # 请求时间在指定时间之前才满足条件
  - Before=2021-09-06T21:51:37.485+08:00[Asia/Shanghai]

# Case 3
predicates:
  # 请求时间在指定时间之间才满足条件
  - Between=2021-09-06T21:51:37.485+08:00[Asia/Shanghai], 2021-09-06T22:30:37.485+08:00[Asia/Shanghai]

其中该配置值需要带时区信息,可通过下述代码获取

public class Test1 {

    @Test
    public static void main(String[] args) {
        ZonedDateTime zonedDateTime = ZonedDateTime.now();
        System.out.println(zonedDateTime);
    }
}

测试结果如下所示

figure 3.jpeg

Cookie

Cookie谓词要求请求携带相应的cookie信息,其还支持正则表达式。配置示例如下所示

# Case 1
predicates:
  # 请求需携带cookie信息,key为id,value为2345
  - Cookie=id, 2345

# Case 2
predicates:
  # 请求需携带cookie信息,key为id,value为一个或多个数字
  - Cookie=id, \d+

这里使用Case 2的谓词进行验证,效果如下,符合预期

figure 4.jpeg

Header

Header谓词要求请求携带相应的请求头,其还支持正则表达式。配置示例如下所示

# Case 1
predicates:
  # 请求需携带请求头属性,属性名为X-Request-Id,value为9978
  - Header=X-Request-Id, 9978

# Case 2
predicates:
  # 请求需携带请求头属性,属性名为X-Request-Id,value为一个或多个数字
  - Header=X-Request-Id, \d+

这里使用Case 2的谓词进行验证,效果如下,符合预期

figure 5.jpeg

Method

Method谓词要求请求的方法类型满足指定类型,其支持多个值。配置示例如下所示

# Case 1
predicates:
  # 请求方法类型需要为 POST 或 GET
  - Method=POST, GET

Filter过滤器

Filter过滤器可以实现对于HTTP请求、响应的修改。根据过滤器的执行时机可分为两类:pre、post,其分别会在请求被执行前和被执行后进行调用。而根据类型可分为两类:GateWayFilter、GlobalFilter。前者作用于某个具体的路由下;后者则会有条件地作用于全部路由

GateWayFilter

对于GateWayFilter而言,其使用方式与Predicate谓词类似,直接在配置文件通过filters进行配置即可。这里以GateWayFilter中的AddRequestParameter过滤器为例进行说明,其会添加参数到请求上。我们对payment_test2_route路由添加该过滤器,配置如下所示

spring:
  cloud:
    gateway:
      routes:
        - id: payment_test2_route
          uri: lb://payment
          predicates:
            - Path=/pay3/test2
          filters:
            # 添加name参数的值为Tony
            - AddRequestParameter=name, Tony

测试结果如下所示,符合预期

figure 6.jpeg

GlobalFilter

而对于GlobalFilter而言,日常更多的是自定义全局过滤器,以满足一些个性化的需求。这里以鉴权为例通过实现GlobalFilter、Ordered接口来自定义一个全局过滤器,实现如下所示

public class CustomGlobalFilter implements GlobalFilterOrdered {

    @Override
    public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 获取请求头Token
        String token = exchange.getRequest().getHeaders().getFirst("token");

        // Token为空, 鉴权失败
        if( StringUtils.isBlank(token) ) {
            exchange.getResponse().setStatusCode( HttpStatus.FORBIDDEN );
            return exchange.getResponse().setComplete();
        }

        // Token校验通过, 继续传递请求
        return chain.filter(exchange);
    }

    /**
     * 值越小, 优先级越高
     * @return
     */

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

测试结果如下所示,符合预期

figure 7.jpeg
浏览 46
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报