Spring Cloud Gateway

网关作为流量的入口,常用的功能包括路由转发,权限校验,限流等;Spring Cloud Gateway 是 Spring Cloud 官方推出的第二代网关框架,定位于取代 Netflix Zuul;相比 Zuul 来说,性能更优,功能更强大;Spring Cloud Gateway 是由 WebFlux + Netty + Reactor 实现的响应式的 api 网关;它不能在传统的 servlet 容器中工作,也不能构建成 war 包;Spring Cloud Gateway 皆在为微服务架构提供一种简单有效的 api 路由的管理方式,并基于 Filter 的方式提供网关的基本功能
官方文档:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gateway-request-predicates-factories

核心概念

  1. 路由(route):路由是网关中最基础的部分,路由信息包括一个 ID、一个目的 URI、一组断言工厂、一组 Filter 组成;若断言为真,则说明请求的 URL 和配置的路由匹配
  2. 断言(predicates):java8 中的断言函数,Spring Cloud Gateway中的断言函数类型是 Spring5.0 框架中的 ServerWebExchange;断言函数允许开发者去定义匹配 Http request 中的任何信息,es:请求头、请求参数等
  3. 过滤器(Filter):Spring Cloud Gateway 中的 Filter 分为 Gateway Filter 和 Global Filter;可以对请求和响应进行处理

工作原理

微服务组件 Gateway - 图1
客户端向 Spring Cloud Gateway 发出请求,若请求与网关程序定义的路由匹配,则该请求就会被发送到网关 Web 处理程序,此时处理程序运行特定的请求过滤器链;过滤器之间用虚线分开的原因是过滤器可能会在发送代理请求的前后执行逻辑;所有 pre 过滤器逻辑先执行,然后执行代理请求;代理请求完成后,执行 post 过滤器逻辑

Spring Cloud Gateway 快速开始

环境搭建

引入依赖:

  1. <!-- gateway网关 -->
  2. <dependency>
  3. <groupId>org.springframework.cloud</groupId>
  4. <artifactId>spring-cloud-starter-gateway</artifactId>
  5. </dependency>
  6. <!-- nacos服务注册与发现 -->
  7. <dependency>
  8. <groupId>com.alibaba.cloud</groupId>
  9. <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
  10. </dependency>

注:会和 spring-webmvc 的依赖冲突,需要排除 spring-webmvc

yml 配置文件:

  1. server:
  2. port: 8888
  3. spring:
  4. application:
  5. name: mall-gateway
  6. #配置nacos注册中心地址
  7. cloud:
  8. nacos:
  9. discovery:
  10. server-addr: 127.0.0.1:8848
  11. gateway:
  12. discovery:
  13. locator:
  14. # 默认为false,设为true开启通过微服务创建路由的功能,即可以通过微服务名访问服务
  15. # http://localhost:8888/mall-order/order/findOrderByUserId/1
  16. enabled: true
  17. # 是否开启网关
  18. enabled: true

路由断言工厂配置

https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gateway-request-predicates-factories
网关启动日志:
image.png

时间配置

可以使用在限时抢购的一些场景中

image.png

  1. spring:
  2. cloud:
  3. gateway:
  4. #设置路由:路由id、路由到微服务的uri、断言
  5. routes:
  6. - id: order_route #路由ID,全局唯一
  7. uri: http://localhost:8020 #目标微服务的请求地址和端口
  8. predicates:
  9. # 测试:http://localhost:8888/order/findOrderByUserId/1
  10. # 匹配在指定的日期时间之后发生的请求 入参是ZonedDateTime类型
  11. - After=2021-01-31T22:22:07.783+08:00[Asia/Shanghai]

获取 ZonedDateTime 类型的指定日期时间

  1. ZonedDateTime zonedDateTime = ZonedDateTime.now();//默认时区
  2. // 用指定时区获取当前时间
  3. ZonedDateTime zonedDateTime2 = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));

设置时间之前发起请求:
image.png
超过设置时间之后再次请求:
image.png

Cookie 匹配

  1. spring:
  2. cloud:
  3. gateway:
  4. #设置路由:路由id、路由到微服务的uri、断言
  5. routes:
  6. - id: order_route #路由ID,全局唯一
  7. uri: http://localhost:8020 #目标微服务的请求地址和端口
  8. predicates:
  9. # Cookie匹配
  10. - Cookie=username, fox

测试:
image.png

Header 匹配

  1. spring:
  2. cloud:
  3. gateway:
  4. #设置路由:路由id、路由到微服务的uri、断言
  5. routes:
  6. - id: order_route #路由ID,全局唯一
  7. uri: http://localhost:8020 #目标微服务的请求地址和端口
  8. predicates:
  9. # Header匹配 请求中带有请求头名为 x-request-id,其值与 \d+ 正则表达式匹配
  10. #- Header=X-Request-Id, \d+

测试:
image.png

路径匹配

spring:
  cloud:
    gateway:
      #设置路由:路由id、路由到微服务的uri、断言
      routes:
      - id: order_route  #路由ID,全局唯一
        uri: http://localhost:8020  #目标微服务的请求地址和端口
        predicates:
         # 测试:http://localhost:8888/order/findOrderByUserId/1
        - Path=/order/**   #Path路径匹配

自定义路由断言工厂

自定义路由断言工厂需要继承 AbstractRoutePredicateFactory 类,重写 apply 方法的逻辑;在 apply 方法中可以通过 exchange.getRequest() 拿到 ServerHttpRequest 对象,从而可以获取得到请求的参数、请求方式、请求头信息等

注:命名需要以 RoutePredicateFactory 结尾

@Component
@Slf4j
public class CheckAuthRoutePredicateFactory extends AbstractRoutePredicateFactory<CheckAuthRoutePredicateFactory.Config> {

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

    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
        return new GatewayPredicate() {

            @Override
            public boolean test(ServerWebExchange serverWebExchange) {
                log.info("调用CheckAuthRoutePredicateFactory" + config.getName());
                if(config.getName().equals("fox")){
                    return true;
                }
                return false;
            }
        };
    }

    /**
     * 快捷配置
     * @return
     */
    @Override
    public List<String> shortcutFieldOrder() {
        return Collections.singletonList("name");
    }

    public static class Config {

        private String name;

        public String getName() {
            return name;
        }

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

yml 配置:

spring:
  cloud:
    gateway:
      #设置路由:路由id、路由到微服务的uri、断言
      routes:
      - id: order_route  #路由ID,全局唯一
        uri: http://localhost:8020  #目标微服务的请求地址和端口
        predicates:
         # 测试:http://localhost:8888/order/findOrderByUserId/1
        - Path=/order/**   #Path路径匹配
        #自定义CheckAuth断言工厂
#        - name: CheckAuth
#          args:
#            name: fox
        - CheckAuth=fox

过滤器工厂配置

Spring Cloud Gateway 内置了很多的过滤器工厂,通过其中的一些可以进行一些业务逻辑处理,es:添加剔除响应头、添加去除参数等
https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gatewayfilter-factories

添加请求头

spring:
  cloud:
    gateway:
      #设置路由:路由id、路由到微服务的uri、断言
      routes:
      - id: order_route  #路由ID,全局唯一
        uri: http://localhost:8020  #目标微服务的请求地址和端口
        #配置过滤器工厂
        filters:
        - AddRequestHeader=X-Request-color, red  #添加请求头

测试:

@GetMapping("/testgateway")
public String testGateway(HttpServletRequest request) throws Exception {
    log.info("gateWay获取请求头X-Request-color:"
            +request.getHeader("X-Request-color"));
    return "success";
}
@GetMapping("/testgateway2")
public String testGateway(@RequestHeader("X-Request-color") String color) throws Exception {
    log.info("gateWay获取请求头X-Request-color:"+color);
    return "success";
}

添加请求参数

spring:
  cloud:
    gateway:
      #设置路由:路由id、路由到微服务的uri、断言
      routes:
      - id: order_route  #路由ID,全局唯一
        uri: http://localhost:8020  #目标微服务的请求地址和端口
        #配置过滤器工厂
        filters:
        - AddRequestParameter=color, blue  # 添加请求参数

测试:

@GetMapping("/testgateway3")
public String testGateway3(@RequestParam("color") String color) throws Exception {
    log.info("gateWay获取请求参数color:"+color);
    return "success";
}

为匹配的路由统一添加前缀

spring:
  cloud:
    gateway:
      #设置路由:路由id、路由到微服务的uri、断言
      routes:
      - id: order_route  #路由ID,全局唯一
        uri: http://localhost:8020  #目标微服务的请求地址和端口
        #配置过滤器工厂
        filters:
        - PrefixPath=/mall-order  # 添加前缀 对应微服务需要配置context-path

对应微服务中配置:

server:
  servlet:
    context-path: /mall-order

重定向操作

spring:
  cloud:
    gateway:
      #设置路由:路由id、路由到微服务的uri、断言
      routes:
      - id: order_route  #路由ID,全局唯一
        uri: http://localhost:8020  #目标微服务的请求地址和端口
        #配置过滤器工厂
        filters:
        - RedirectTo=302, https://www.baidu.com/  #重定向到百度

自定义过滤器工厂

继承 AbstractNameValueGatewayFilterFactory 且自定义的名称必须以 GatewayFilterFactory 结尾并交给 Spring 管理

@Component
@Slf4j
public class CheckAuthGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {

    @Override
    public GatewayFilter apply(NameValueConfig config) {
        return (exchange, chain) -> {
            log.info("调用CheckAuthGatewayFilterFactory==="
                    + config.getName() + ":" + config.getValue());
            return chain.filter(exchange);
        };
    }
}

配置指定以的过滤器工厂:

spring:
  cloud:
    gateway:
      #设置路由:路由id、路由到微服务的uri、断言
      routes:
      - id: order_route  #路由ID,全局唯一
        uri: http://localhost:8020  #目标微服务的请求地址和端口
        #配置过滤器工厂
        filters:
        - CheckAuth=fox,男

全局过滤器配置

GlobalFilter 接口和 GatewayFilter 有一样的接口定义,只不过 GlobalFilter 会作用于所有路由;
官方声明:GlobalFilter 的接口定义以及用法在未来的版本可能会发生变化

LoadBalancerClientFilter

LoadBalancerClientFilter 会查看 exchange 的属性 ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR 的值,若该值的 scheme 是 lb,es:lb:myservice,将会使用 Spring Cloud 的 LoadBalancerClient 来将 myservice 解析成实际的 host 和 port,并替换掉 ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR 的内容

其实就是用来整合负载均衡器 Ribbon 的

spring:
  cloud:
    gateway:
      routes:
      - id: order_route
        uri: lb://mall-order
        predicates:
        - Path=/order/**

自定义全局过滤器

@Component
@Order(-1)
@Slf4j
public class CheckAuthFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //校验请求头中的token
        List<String> token = exchange.getRequest().getHeaders().get("token");
        log.info("token:"+ token);
        if (token.isEmpty()){
            return null;
        }
        return chain.filter(exchange);
    }
}

@Component
public class CheckIPFilter implements GlobalFilter, Ordered {

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

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        HttpHeaders headers = exchange.getRequest().getHeaders();
        //模拟对 IP 的访问限制,即不在 IP 白名单中就不能调用的需求
        if (getIp(headers).equals("127.0.0.1")) {
            return null;
        }
        return chain.filter(exchange);
    }

    private String getIp(HttpHeaders headers) {
        return headers.getHost().getHostName();
    }
}

Gateway 跨域配置(CORS Configuration)

通过 yml 配置的方式:
https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#cors-configuration

spring:
  cloud:
    gateway:
        globalcors:
          cors-configurations:
            '[/**]':
              allowedOrigins: "*"
              allowedMethods:
              - GET
              - POST
              - DELETE
              - PUT
              - OPTION

通过 java 配置的方式:
image.png

@Configuration
public class CorsConfig {
    @Bean
    public CorsWebFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedMethod("*");
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
        source.registerCorsConfiguration("/**", config);

        return new CorsWebFilter(source);
    }
}

Gateway 整合 Sentinel 限流

https://github.com/alibaba/Sentinel/wiki
从 1.6.0 版本开始,Sentinel 提供了 Spring Cloud Gateway 的适配模块,可以提供两种资源维度的限流:

  1. route 维度:在 Spring 配置文件中配置的路由条目,资源名为对应的 routeId
  2. 自定义 api 维度:用户可以利用 Sentinel 提供的 api 来自定义一些 api 分组

引入依赖:

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
    <version>x.y.z</version>
</dependency>

接入 Sentinel Dashboard,添加 yml 配置:

spring:
  application:
    name: mall-gateway-sentinel-demo
  #配置nacos注册中心地址
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

    sentinel:
      transport:
        # 添加sentinel的控制台地址
        dashboard: 127.0.0.1:8080

使用时只需注入对应的 SentinelGatewayFilter 实例以及 SentinelGatewayBlockExceptionHandler 实例即可:

@Configuration
public class GatewayConfiguration {

    private final List<ViewResolver> viewResolvers;
    private final ServerCodecConfigurer serverCodecConfigurer;

    public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
                                ServerCodecConfigurer serverCodecConfigurer) {
        this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
        this.serverCodecConfigurer = serverCodecConfigurer;
    }

    /**
     * 限流异常处理器
     * @return
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
        // Register the block exception handler for Spring Cloud Gateway.
        return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
    }

    /**
     * 限流过滤器
     * @return
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public GlobalFilter sentinelGatewayFilter() {
        return new SentinelGatewayFilter();
    }
}

可通过 GatewayRuleManager.loadRules(rules) 手动加载网关规则,GatewayConfiguration 中添加:

@PostConstruct
    public void doInit() {
        //初始化自定义的API
        initCustomizedApis();
        //初始化网关限流规则
        initGatewayRules();
        //自定义限流异常处理器
        initBlockRequestHandler();
    }

    private void initCustomizedApis() {
        Set<ApiDefinition> definitions = new HashSet<>();
        ApiDefinition api = new ApiDefinition("user_service_api")
                .setPredicateItems(new HashSet<ApiPredicateItem>() {{
                    add(new ApiPathPredicateItem().setPattern("/user/**")
                            .setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
                }});
        definitions.add(api);
        GatewayApiDefinitionManager.loadApiDefinitions(definitions);
    }

    private void initGatewayRules() {
        Set<GatewayFlowRule> rules = new HashSet<>();
        //resource:资源名称,可以是网关中的 route 名称或者用户自定义的 API 分组名称。
        //count:限流阈值
        //intervalSec:统计时间窗口,单位是秒,默认是 1 秒。
        rules.add(new GatewayFlowRule("order_route")
                .setCount(2)
                .setIntervalSec(1)
        );
        rules.add(new GatewayFlowRule("user_service_api")
                .setCount(2)
                .setIntervalSec(1)
        );

        // 加载网关规则
        GatewayRuleManager.loadRules(rules);
    }

    private void initBlockRequestHandler() {
        BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
            @Override
            public Mono<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable t) {
                HashMap<String, String> result = new HashMap<>();
                result.put("code",String.valueOf(HttpStatus.TOO_MANY_REQUESTS.value()));
                result.put("msg", HttpStatus.TOO_MANY_REQUESTS.getReasonPhrase());

                return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
                        .contentType(MediaType.APPLICATION_JSON)
                        .body(BodyInserters.fromValue(result));
            }
        };
        //设置自定义异常处理器
        GatewayCallbackManager.setBlockHandler(blockRequestHandler);
    }

网关流控控制台

Sentinel 1.6.3 引入了网关流控控制台的支持,可直接在 Sentinel 控制台上查看 API Gateway 实时的 route 和自定义 api 分组监控,管理网关规则和 api 分组配置;在 API Gateway 端,只需要在原有启动参数的基础上添加如下启动参数即可标记应用为 API Gateway 类型:

# 注:通过 Spring Cloud Alibaba Sentinel 自动接入的 API Gateway 整合则无需此参数
-Dcsp.sentinel.app.type=1

网关流控实现原理

image.png

网关高可用

为了保证 Gateway 的高可用性,可同时启动多个 Gateway 实例进行负载,在 Gateway 的上游使用 Nginx 或者 F5 进行负载转发以达到高可用
image.png