spring-cloud-gateway 服务网关

  Spring Cloud Gateway是Spring Cloud官方推出的第二代网关框架,取代Zuul网关。网关作为流量的,在微服务系统中有着非常作用,网关常见的功能有路由转发、权限校验、限流控制等作用。

  Spring Cloud Gateway是Spring官方最新推出的一款基于Spring Framework 5,Project Reactor和Spring Boot 2之上开发的网关。与zuul1.0不同的是,gateway是异步非阻塞的(netty+webflux实现),zuul1.0是同步阻塞请求的。gateway的数据是封装在ServerWebExchange中,zuul是存放在RequestContext里的(这里是重点,圈起来!)Gateway相对于Zuul来说,在路由的配置上更加多样化,配置更加简便。

  官方文档 : https://docs.spring.io/spring-cloud-gateway/docs/2.2.5.RELEASE/reference/html/#gateway-starter

Gateway 核心概念:

  1. Route 路由,它是网关的基础元素,包含ID、目标URI、断言、过滤器组成,当前请求到达网关时,会通过Gateway Handler Mapping,基于断言进行路由匹配,当断言为true时,匹配到路由进行转发

  2. Predicate,断言,学过java8的同学应该知道这个函数,它可以允许开发人员去匹配HTTP请求中的元素,一旦匹配为true,则表示匹配到合适的路由进行转发

  3. Filter,过滤器,可以在请求发出的前后进行一些业务上的处理,比如授权、埋点、限流等。

Gateway 工作模型:

  其中,predicate就是我们的匹配条件;而filter,就可以理解为一个无所不能的拦截器。有了这两个元素,再加上目标uri,就可以实现一个具体的路由了。客户端向 Spring Cloud Gateway 发出请求,如果请求与网关程序定义的路由匹配,则该请求就会被发送到网关 Web 处理程序,此时处理程序运行特定的请求过滤器链。过滤器之间用虚线分开的原因是过滤器可能会在发送代理请求的前后执行逻辑。所有 pre 过滤器逻辑先执行,然后执行代理请求;代理请求完成后,执行 post 过滤器逻辑。

Predicate 路由断言:

  Spring Cloud Gateway将路由匹配为Spring WebFlux HandlerMapping基础设施的一部分。Spring Cloud Gateway包括许多内置的路由谓词工厂。所有这些谓词都匹配HTTP请求的不同属性。您可以使用逻辑和语句组合多个路由谓词工厂。

  Gateway 的路由断言机制以 AbstractRoutePredicateFactory 为基础,实现了如下多种方式。再官方文档内提供了配置说明

Gateway 网关的实现:

  要在项目中引入Spring Cloud Gateway,引入相关依赖,然后只需要一些简单的配置即可构建好一个 Gateway 网关服务。

1.引入依赖(基于 spring-cloud-dependencies  Hoxton.SR4 版本)

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

2. 以 PathRoutePredicateFactory 路由断言为例子演示,做以下配置:

server:
  port: 9544

spring:
  application:
    name: gateway-service
  cloud:
    gateway:
      enabled: true
      discovery:
        locator:
          enabled: false #gateway开启服务注册和发现的功能
          lowerCaseServiceId: true #请求路径上的服务名配置为小写
      routes:
        - id: ribbon-server
          uri: lb://RIBBON-SERVER #uri以lb://开头(lb代表从注册中心获取服务),后面接的就是你需要转发到的服务名称
          predicates:
            - Path=/demo/**
          filters:
            - StripPrefix=1 # 代表 Path 的值中将第一段舍弃,本例子就是转发的时候为 /** 将/demo 去除。

eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka/
  instance:
    instance-id: gateway-service

3.启动服务访问相关的路径即可看到效果。

  自定义Predicate 路由断言的实现:

  创建一个类,继承 AbstractRoutePredicateFactory 类,实现对应方法:

@Component
public class AuthRoutePredicateFactory extends AbstractRoutePredicateFactory<AuthRoutePredicateFactory.Config> {

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

    private static final String NAME_KEY = "name";
    private static final String VALUE_KEY = "value";

    @Override
    public List<String> shortcutFieldOrder() {
        //属性进行匹配对应
        return Arrays.asList(NAME_KEY, VALUE_KEY);
    }

    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
        //Header中携带了某个值,进行header的判断
        return (exchange -> {
            HttpHeaders headers = exchange.getRequest().getHeaders();
            List<String> headerList = headers.get(config.getName());
            return headerList.size() > 0;
        });
    }

    public static class Config {
        private String name;
        private String value;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getValue() {
            return value;
        }

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

  yml 配置如下:

spring:
  application:
    name: gateway-service
  cloud:
    gateway:
      enabled: true
      discovery:
        locator:
          enabled: false #gateway开启服务注册和发现的功能
          lowerCaseServiceId: true #请求路径上的服务名配置为小写
      routes:
        - id: cookie_route
          predicates:
            - Auth=Authorization,token
          filters:
            - StripPrefix=1
          uri: lb://RIBBON-SERVER

  其中 Authorization 为 name,token 为 value,打个断点看看就行了。

Filter 请求过滤器:

  Filter分为全局过滤器和路由过滤器,当请求与路由匹配时,过滤Web处理程序会将的所有实例GlobalFilter和所有特定GatewayFilter于路由的实例添加到过滤器链中。该组合的过滤器链按org.springframework.core.Ordered接口排序,您可以通过实现该getOrder()方法进行设置。

  需要获取到更详细的信息可以惨开官网 :https://docs.spring.io/spring-cloud-gateway/docs/2.2.5.RELEASE/reference/html/#gatewayfilter-factories

  这里演示一下自定义的过滤器(全局、路由)

1.全局过滤器只需要实现接口  org.springframework.cloud.gateway.filter.GlobalFilter,而后无需任何配置,即可生效。

@Component
public class CustomGlobalFilter implements GlobalFilter, Ordered {

    Logger logger= LoggerFactory.getLogger(CustomGlobalFilter.class);

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        logger.info("custom global filter");
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return -1;
    }
}

2.路由过滤器 需要继承  org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory

每个过滤器工厂都对应一个实现类,并且这些类的名称必须以 GatewayFilterFactory 结尾,这是Spring Cloud Gateway的一个约定,例如 AddRequestHeader 对应的实现类为 AddRequestHeaderGatewayFilterFactory 

@Component
public class WuzzDefineGatewayFilterFactory extends AbstractGatewayFilterFactory<WuzzDefineGatewayFilterFactory.WuzzConfig>{

    private static final String NAME_KEY="name";

    Logger logger= LoggerFactory.getLogger(WuzzDefineGatewayFilterFactory.class);

    public WuzzDefineGatewayFilterFactory() {
        super(WuzzConfig.class);
    }

    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList(NAME_KEY);
    }

    @Override
    public GatewayFilter apply(WuzzConfig config) {
        //Filter pre  post
        return ((exchange,chain)->{
            logger.info("[pre] Filter Request, name:"+config.getName());
            //TODO
            return chain.filter(exchange).then(Mono.fromRunnable(()->{
                //TODO
                logger.info("[post]: Response Filter");
            }));
        });
    }

    public static class WuzzConfig{
        private String name;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }
}

3. 配置路由规则

spring:
  application:
    name: gateway-service
  cloud:
    gateway:
      enabled: true
      discovery:
        locator:
          enabled: false #gateway\u5F00\u542F\u670D\u52A1\u6CE8\u518C\u548C\u53D1\u73B0\u7684\u529F\u80FD
          lowerCaseServiceId: true #\u8BF7\u6C42\u8DEF\u5F84\u4E0A\u7684\u670D\u52A1\u540D\u914D\u7F6E\u4E3A\u5C0F\u5199
      routes:
        - id: config_route
          predicates:
            - Path=/demo/**
          filters:
            - StripPrefix=1
            - WuzzDefine=Hello Wuzz
          uri: lb://RIBBON-SERVER

4. 启动测试,发送一个请求,可以在控制台看到如下信息,说明过滤器均生效:

  使用自带的限流过滤器 :

spring:
  application:
    name: gateway-service
  cloud:
    gateway:
      enabled: true
      discovery:
        locator:
          enabled: false #gateway\u5F00\u542F\u670D\u52A1\u6CE8\u518C\u548C\u53D1\u73B0\u7684\u529F\u80FD
          lowerCaseServiceId: true #\u8BF7\u6C42\u8DEF\u5F84\u4E0A\u7684\u670D\u52A1\u540D\u914D\u7F6E\u4E3A\u5C0F\u5199
      routes:
        - id: config_route
          predicates:
            - Path=/demo/**
          filters:
            - StripPrefix=1
            - WuzzDefine=Hello Wuzz
          uri: lb://RIBBON-SERVER
        - id: ratelimiter_route
          predicates:
            - Path=/ratelimiter/**
          filters:
            - StripPrefix=1
            - name: RequestRateLimiter
              args:
                deny-empty-key: true
                keyResolver: '#{@ipAddressKeyResolver}'
                redis-rate-limiter.replenishRate: 1
                redis-rate-limiter.burstCapacity: 2
          uri: lb://RIBBON-SERVER

  以上配置了限流过滤器,

  1. replenishRate:令牌桶中令牌的填充速度,代表允许每秒执行的请求数。
  2. burstCapacity:令牌桶的容量,也就是令牌桶最多能够容纳的令牌数。表示每秒用户最大能够执行的请求数量。

  其中还需要配置一个   keyResolver:

@Component
public class IpAddressKeyResolver implements KeyResolver{

    @Override
    public Mono<String> resolve(ServerWebExchange exchange) {
        return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
    }
}

  然后启动测试,迅速刷新页面访问接口,会限流:

动态路由:

  Spring Cloud Gateway 提供了 Endpoint 端点,暴露路由信息,有获取所有路由、刷新路由、查看单个路由、删除路由等方法,源码在 org.springframework.cloud.gateway.actuate.GatewayControllerEndpoint 中,想访问端点中的方法需要添加 spring-boot-starter-actuator 依赖,并在配置文件中暴露所有端点

management:
  endpoints:
    web:
      exposure:
        include: "*"

  列举几个常用的操作API 。详细信息及配置查看官方文档:https://docs.spring.io/spring-cloud-gateway/docs/2.2.5.RELEASE/reference/html/#actuator-api

  • /actuator/gateway/routes GET 获取路由列表
  • /actuator/gateway/globalfilters  GET 全局过滤器列表
  • /actuator/gateway/routefilters  GET 路由过滤器列表
  • /actuator/gateway/refresh   POST  刷新新增的路由
  • /gateway/routes/{id_route_to_create}   POST/DELETS  新增或者删除
  • ......

  新增路由示例 :

  然后调用一下 刷新的接口,即可生效。但是默认情况下,我们新增的路由只是保存在内存中,万一服务重启,则配置信息丢失,这个时候就需要将路由信息持久化。

  路由持久化--基于redis

1. 引入 redis 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
</dependency>

2. 实现 org.springframework.cloud.gateway.route.RouteDefinitionRepository 接口,重写路由的相关操作方法

@Component
public class RedisRouteDefinitionRepository implements RouteDefinitionRepository {

    private final static String GATEWAY_ROUTE_KEY="gateway_dynamic_route";

    @Autowired
    RedisTemplate<String,String> redisTemplate;

    @Override
    public Flux<RouteDefinition> getRouteDefinitions() {
        List<RouteDefinition> routeDefinitionList=new ArrayList<>();
        redisTemplate.opsForHash().values(GATEWAY_ROUTE_KEY).stream().forEach(route->{
            routeDefinitionList.add(JSON.parseObject(route.toString(),RouteDefinition.class));
        });
        return Flux.fromIterable(routeDefinitionList);
    }

    @Override
    public Mono<Void> save(Mono<RouteDefinition> route) {
        return route.flatMap(routeDefinition -> {
            redisTemplate.opsForHash().put(GATEWAY_ROUTE_KEY,routeDefinition.getId(),JSON.toJSONString(routeDefinition));
            return Mono.empty();
        });
    }

    @Override
    public Mono<Void> delete(Mono<String> routeId) {
        return routeId.flatMap(id->{
            if(redisTemplate.opsForHash().hasKey(GATEWAY_ROUTE_KEY,id)){
                redisTemplate.opsForHash().delete(GATEWAY_ROUTE_KEY,id);
                return Mono.empty();
            }
            return Mono.defer(()->Mono.error(new Exception("routeDefinition not found:"+routeId)));
        });
    }
}

3.配置路由

spring:
  application:
    name: gateway-service
  cloud:
    gateway:
      enabled: true
      discovery:
        locator:
          enabled: false #gateway\u5F00\u542F\u670D\u52A1\u6CE8\u518C\u548C\u53D1\u73B0\u7684\u529F\u80FD
          lowerCaseServiceId: true #\u8BF7\u6C42\u8DEF\u5F84\u4E0A\u7684\u670D\u52A1\u540D\u914D\u7F6E\u4E3A\u5C0F\u5199
      routes:
        - id: config_route
          predicates:
            - Path=/demo/**
          filters:
            - StripPrefix=1
            - WuzzDefine=Hello Wuzz
          uri: lb://RIBBON-SERVER

4. 测试,启动后按照之前一样创建一个路由,这个时候发现 redis里面已经保存了这个路由配置信息

  也可以通过配置中心,比如 config 、Nacos 实现动态的配置。

posted @ 2020-09-25 10:00  吴振照  阅读(1602)  评论(0编辑  收藏  举报