Spring Cloud的gateway网关配置

一、网关作用

​ 1. 统一服务的对外入口,鉴权,跨域,日志;

​ 2. 统一过滤与拦截;

​ 3. 动态路由;

​ 4. 限流熔断。

二、网关原理

  1. 网关的核心概念
    • Route:网关的基础元素,由 ID、目标 URI、断言、过滤器组成。
    • Predicate:匹配条件。
    • Filter:过滤器
  1. 网关请求链路

    当网关接受到请求后,会在Gateway Handler Mapping中找到与请求相匹配的路径,将请求转发到Gateway Web Handler中,Handler在通过路由指定的Filter过滤器,判断过滤器的类型,如果是pre过滤器,则执行完过滤器在执行代理请求,如果是post过滤器,则先执行代理请求在执行过滤器。

三、网关接入

pom.xml加入依赖

        <!-- spring cloud gateway 网关-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!-- spring cloud alibaba sentinel 网关整合-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
        </dependency>

这里有个要注意的点是gateway是依赖spring-boot-starter-webflux实现的,与spring-boot-starter-web会有冲突。所以在网关项目中不要引用spring-boot-starter-web,或者排除它。

全部pom配置:

    <dependencies>
        <dependency>
            <groupId>cn.wisefly</groupId>
            <artifactId>business-wcenter-sdk</artifactId>
        </dependency>
        <!--引入配置中心阿里巴巴-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <!--引入注册中心阿里巴巴-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--sentinel 熔断-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <!-- sentinel nacos 数据源-->
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-nacos</artifactId>
        </dependency>
        <!-- spring cloud gateway 网关-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!-- spring cloud alibaba sentinel 网关整合-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
        </dependency>
        <!-- spring boot actuator-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--SpringBoot 监控客户端-->
        <dependency>
            <groupId>de.codecentric</groupId>
            <artifactId>spring-boot-admin-starter-client</artifactId>
        </dependency>
        <!--验证码 -->
        <dependency>
            <groupId>com.github.penggle</groupId>
            <artifactId>kaptcha</artifactId>
        </dependency>
        <!-- Swagger -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>${swagger.fox.version}</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>${swagger.fox.version}</version>
        </dependency>
        <!-- SpringCloud Zipkin -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zipkin</artifactId>
        </dependency>
        <!--kafka -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-stream-kafka</artifactId>
        </dependency>
        <!-- kafka client-->
        <dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <!-- 指定项目编译时的java版本和编码方式 -->
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                            <goal>build-info</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <!--新增的docker maven插件-->
            <plugin>
                <groupId>com.spotify</groupId>
                <artifactId>docker-maven-plugin</artifactId>
                <version>${docker-maven-plugin.version}</version>
                <!--docker镜像相关的配置信息-->
                <configuration>
                    <!--镜像名,要带上私有服务器IP和端口-->
                    <imageName>${docker-registryUrl}/${project.artifactId}:${project.version}</imageName>
                    <!--TAG,这里用工程版本号-->
                    <imageTags>
                        <imageTag>${project.version}</imageTag>
                        <imageTag>latest</imageTag>
                    </imageTags>
                    <!--Dockerfile文件地址-->
                    <dockerDirectory>${project.basedir}</dockerDirectory>
                    <!--构建镜像的配置信息-->
                    <resources>
                        <resource>
                            <targetPath>/</targetPath>
                            <!--指定复制jar包的根目录-->
                            <directory>${project.build.directory}</directory>
                            <!--指定复制的文件-->
                            <include>${project.build.finalName}.jar</include>
                        </resource>
                    </resources>
                    <!--指定推送的仓库-->
                    <registryUrl>${docker-registryUrl}</registryUrl>
                    <!-- 开启远程API -->
                    <dockerHost>${docker-dockerHost}</dockerHost>
                    <!-- 是否有push功能 -->
                    <pushImage>true</pushImage>
                    <!--push后是否覆盖存在的标签镜像-->
                    <forceTags>true</forceTags>
                </configuration>
            </plugin>
        </plugins>
    </build>

配置文件加入

           Spring:

               cloud:

                     # 路由网关配置

                    gateway:

                     # 设置与服务注册发现组件结合,这样可以采用服务名的路由策略

                        discovery:

                           locator:

                              enabled: true

                              # 配置之后访问时无需大写
                              lower-case-service-id: true

                        #路由配置

                         routes:

                             - id: file-route
                                uri: lb://wisefly-file
                                predicates:
                                   - Path=/file/v1.0/**

                        default-filters:

                            - StripPrefix=0

nacos上的全部配置:

展开查看
 
spring:
  cloud:
    # 使用 Naoos 作为服务注册发现
    nacos:
      discovery:
        server-addr: 10.0.1.104:8848
        namespace: 09dd3d79-92c0-47fe-bd1d-0f1b7f8dc928
        metadata:
          management:
            context-path: ${server.servlet.context-path}/actuator
    sentinel:
      transport:
        port: 8730
        dashboard: 10.0.1.104:8080
        # 服务启动直接建立心跳连接
      eager: true
      # Sentinel Nacos 数据源
      datasource:
        ds:
          nacos:
            server-addr: 10.0.1.104:8848
            groupId: DEFAULT_GROUP
            dataId: gateway-sentinel
            namespace: 09dd3d79-92c0-47fe-bd1d-0f1b7f8dc928
            rule-type: flow
    # 路由网关配置
    gateway:
      # 设置与服务注册发现组件结合,这样可以采用服务名的路由策略
      discovery:
        locator:
          enabled: true
          # 配置之后访问时无需大写
          lower-case-service-id: true
      routes:
        - id: file-route
          uri: lb://wisefly-file
          predicates:
            - Path=/file/v1.0/**
        - id: workflow-route
          uri: lb://wisefly-workflow
          predicates:
            - Path=/workflow/v1.0/**
        - id: wcenter-route
          uri: lb://wisefly-wcenter
          predicates:
            - Path=/wcenter/v1.0/**
        - id: resource-route
          uri: lb://wisefly-resource
          predicates:
            - Path=/resource/v1.0/**
        - id: inspection-route
          uri: lb://wisefly-inspection
          predicates:
            - Path=/inspection/v1.0/**
        - id: criterion-route
          uri: lb://wisefly-criterion
          predicates:
            - Path=/criterion/v1.0/**
        - id: hbase-route
          uri: lb://wisefly-hbase
          predicates:
            - Path=/hbase/v1.0/**
        - id: turnhospital-route
          uri: lb://wisefly-turnhospital
          predicates:
            - Path=/turnhospital/v1.0/**
        - id: cms-route
          uri: lb://business-cms
          predicates:
            - Path=/cms/v1.0/**
      default-filters:
        - StripPrefix=0
    #自定义过滤的地址
    filter:
      url:
        whites: 
          - /wcenter/v1.0/authentication/login
          - /v2/api-docs
  # redis 配置
  redis:
    # 地址
    host: 10.0.1.104
    # 端口,默认为6379
    port: 6379
    # 密码
    password: 123456
    # 连接超时时间
    timeout: 10s
    lettuce:
      pool:
        # 连接池中的最小空闲连接
        min-idle: 0
        # 连接池中的最大空闲连接
        max-idle: 8
        # 连接池的最大数据库连接数
        max-active: 8
        # #连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1ms
  

四、动态网关实现

framework2.0动态路由是基础nacos配置中心实现动态路由配置以及存储。具体实现在cn.wisefly.gateway.route包下面,主要有三个要点,大概说下:

动态路由nacos配置类

package cn.wisefly.gateway.route.config;


import cn.wisefly.gateway.handler.SentinelFallbackHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;

@Configuration
public class GatewayConfig {

  public static final long DEFAULT_TIMEOUT = 30000;
  @Value("${spring.cloud.nacos.config.server-addr}")
  private String address;
  @Value("${spring.cloud.nacos.config.namespace}")
  private String namespace;
  @Value("${nacos.gateway.route.config.data-id}")
  private String dataId;
  @Value("${nacos.gateway.route.config.group}")
  private String groupId;

  @Bean
  @Order(Ordered.HIGHEST_PRECEDENCE)
  public SentinelFallbackHandler sentinelGatewayExceptionHandler() {
    return new SentinelFallbackHandler();
  }

  @Bean
  @Order(-1)
  public GlobalFilter sentinelGatewayFilter() {
    return new SentinelGatewayFilter();
  }

  public String getNamespace() {
    return namespace;
  }

  public void setNamespace(String namespace) {
    this.namespace = namespace;
  }

  public String getAddress() {
    return address;
  }

  public void setAddress(String address) {
    this.address = address;
  }

  public String getDataId() {
    return dataId;
  }

  public void setDataId(String dataId) {
    this.dataId = dataId;
  }

  public String getGroupId() {
    return groupId;
  }

  public void setGroupId(String groupId) {
    this.groupId = groupId;
  }

}

初始化路由,监听nacos配置变化

监听是利用了nacos-api提供的ConfigService.addListener来实现的。

package cn.wisefly.gateway.route.nacos;

import cn.wisefly.gateway.route.config.GatewayConfig;
import cn.wisefly.gateway.route.service.DynamicRouteServiceImpl;
import com.alibaba.fastjson.JSON;
import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import javax.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class DynamicRouteServiceImplByNacos {

  //定义全局静态MAP
  private static Map<String, List<RouteDefinition>> map = new ConcurrentHashMap<>();

  @Autowired
  private DynamicRouteServiceImpl dynamicRouteService;

  @Autowired
  private GatewayConfig gatewayConfig;

  private ConfigService configService;

  @PostConstruct
  public void init() {
    log.info("gateway route init...");
    try {
      configService = initConfigService();
      if (configService == null) {
        log.warn("initConfigService fail");
        return;
      }
      String configInfo = configService.getConfig(gatewayConfig.getDataId(), gatewayConfig.getGroupId(), GatewayConfig.DEFAULT_TIMEOUT);
      log.info("获取网关当前配置:\r\n{}", configInfo);
      List<RouteDefinition> definitionList = JSON.parseArray(configInfo, RouteDefinition.class);
      for (RouteDefinition definition : definitionList) {
        log.info("update route : {}", definition.toString());
        dynamicRouteService.add(definition);
      }
      //放入全局MAP中
      map.put(gatewayConfig.getDataId(), definitionList);
    } catch (Exception e) {
      log.error("初始化网关路由时发生错误", e);
    }
    dynamicRouteByNacosListener(gatewayConfig.getDataId(), gatewayConfig.getGroupId());
  }

  /**
   * 监听Nacos下发的动态路由配置
   *
   * @param dataId
   * @param group
   */
  public void dynamicRouteByNacosListener(String dataId, String group) {
    try {
      configService.addListener(dataId, group, new Listener() {
        @Override
        public void receiveConfigInfo(String configInfo) {
          log.info("进行网关更新:\n\r{}", configInfo);
          List<RouteDefinition> definitionList = JSON.parseArray(configInfo, RouteDefinition.class);
          for (RouteDefinition definition : definitionList) {
            log.info("update route : {}", definition.toString());
            dynamicRouteService.update(definition);
          }
          //更具dataId获取map里面存在的route
          List<RouteDefinition> staticRouteMap = map.get(dataId);
          if (staticRouteMap != null) {
            //比较与监听到的路由配置是否一致(只比较ID)
            for (RouteDefinition mapRoute : staticRouteMap) {
              Boolean bool = false;
              for (RouteDefinition definition : definitionList) {
                //如果MAP的数据在动态获取的数据中不存在,则表示此次动态删除了
                if (mapRoute.getId().equals(definition.getId())) {
                  bool = true;
                  break;
                }
                //如果为false则主动触发删除操作
                if (!bool) {
                  dynamicRouteService.delete(mapRoute.getId());
                }
              }
            }
          }
          //将最新的动态数据放入map
          map.put(dataId, definitionList);
        }

        @Override
        public Executor getExecutor() {
          log.info("getExecutor\n\r");
          return null;
        }
      });
    } catch (NacosException e) {
      log.error("从nacos接收动态路由配置出错!!!", e);
    }
  }

  /**
   * 初始化网关路由 nacos config
   *
   * @return
   */
  private ConfigService initConfigService() {
    try {
      Properties properties = new Properties();
      properties.setProperty("serverAddr", gatewayConfig.getAddress());
      properties.setProperty("namespace", gatewayConfig.getNamespace());
      return configService = NacosFactory.createConfigService(properties);
    } catch (Exception e) {
      log.error("初始化网关路由时发生错误", e);
      return null;
    }
  }
}

动态更新路由网关

这里的核心是实现Spring提供的事件推送接口ApplicationEventPublisherAware。

package cn.wisefly.gateway.route.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

@Service
@Slf4j
public class DynamicRouteServiceImpl implements ApplicationEventPublisherAware {

  @Autowired
  private RouteDefinitionWriter routeDefinitionWriter;

  private ApplicationEventPublisher publisher;

  /**
   * 增加路由
   *
   * @param definition
   * @return
   */
  public String add(RouteDefinition definition) {
    routeDefinitionWriter.save(Mono.just(definition)).subscribe();
    this.publisher.publishEvent(new RefreshRoutesEvent(this));
    return "success";
  }
  /**
   * 更新路由
   *
   * @param definition
   * @return
   */
  public String update(RouteDefinition definition) {
    try {
      this.routeDefinitionWriter.delete(Mono.just(definition.getId()));
    } catch (Exception e) {
      return "update fail,not find route routeId: " + definition.getId();
    }
    try {
      routeDefinitionWriter.save(Mono.just(definition)).subscribe();
      this.publisher.publishEvent(new RefreshRoutesEvent(this));
      return "success";
    } catch (Exception e) {
      return "update route fail";
    }
  }
  /**
   * 删除路由
   *
   * @param id
   * @return
   */
  public String delete(String id) {
    try {
      this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();
      return "delete success";
    } catch (Exception e) {
      e.printStackTrace();
      return "delete fail";
    }
  }

  @Override
  public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
    this.publisher = applicationEventPublisher;
  }
}

五、网关路由配置

网关模块搭建完成后,改动最频繁的就是路由配置了,目前我们的路由配置在配置中心的wisefly-gateway-dev.yaml文件中。配置规则如下:

      #路由配置

      routes:

         - id: file-route #路由ID,不能重复
           uri: lb://wisefly-file #请求转发的路径,一般配置成服务名称即可。
           predicates: #断言
               - Path=/file/v1.0/** #拦截的请求方法的路径,一般配置与context-path相同,如果不相同的话swagger无法接入网关中。

以上路由规则配置完成后,访问file模块的请求路径只需要为http://网关ip:网关port/file/v1.0/**就能通过网关转发到业务请求(当然不做以上配置的话也是能通过http://网关ip:网关port/服务名称/context-path/**通过网关转发业务请求)。


   转载规则


《Spring Cloud的gateway网关配置》 锦泉 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录