目录
  • (一)什么是微服务网关
  • (二)Spring Cloud Gateway网关
    • 2.1 核心概念:
    • 2.2 搭建环境:
  • (三) 路由配置详解
    • 3.1 自定义断言配置
    • 3.2 断言不匹配404页面自定义
  • (四)Spring Cloud Gateway过滤器
    • (五) 网关限流
      • 5.1 常见的一些限流算法:
      • 5.2 集成Sentinel进行限流
      • 5.3 网关实现跨域
    • (六)总结

      (一)什么是微服务网关

      后端写完所有的微服务之后,最终是要交给前端去调用。我们都知道每个微服务都有各自的端口号,如果前端直接通过IP加端口的方式去调用微服务会很麻烦。如果想对请求增加限制也会变得十分困难。这个时候微服务网关就出现了。

      微服务网关又成为API网关,它是系统对外的唯一入口。APl网关封装了系统内部架构,为每个客户端提供一个定制的APl。API网关方式的核心要点是,所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有的非业务功能。通常,网关也是提供REST/HTTP的访问API。服务端通过API-GW注册和管理服务。

      听起来有些抽象,下面就做一张图给大家展示一下:前端的请求统一由微服务网关进行管理,微服务网关可以调用各个微服务,同时自身还有多种功能。

      基于SpringCloudGateway实现微服务网关的方式

      API网关的职责有身份验证、监控、负载均衡、缓存、请求分片与管理、静态响应处理,最重要的功能是和外界联系。

      常见的API网关实现方式有Zuul,traefix,Spring Cloud Gateway等等。目前主流的微服务网关是Spring Cloud Gateway。

      (二)Spring Cloud Gateway网关

      Spring Cloud Gateway是Spring 官方基于Spring 5.0,Spring Boot 2.0和Project Reactor 等技术开发的网关,旨在为微服务架构提供一种简单而有效的统一的API路由管理方式,统一访问接口。Spring Cloud Gateway作为Spring Cloud 生态系中的网关,目标是替代Netflix ZUUL,其不仅提供统一的路由方式,并且基于Filter链的方式提供了网关基本的功能,例如:安全,监控/埋点,和限流等。

      2.1 核心概念:

      路由(route):路由是网关最基础的部分,路由信息由一个ID,一个目的URL,一组断言工厂和一组Filter组成。

      断言(predicates):java8中的断言函数,Spring Cloud Gateway中的断言函数允许开发者去定义匹配来自Http Request中的任何信息。当断言为真时,则匹配路由。

      过滤器(filter):对请求和响应进行过滤

      下面通过实践来操作微服务网关,本项目有依赖于Nacos与Sentinel,如果不需要这部分的内容可以在配置文件中去除。

      2.2 搭建环境:

      1、引入工程导入依赖

      创建api_gateway_server工程,在工程中的pom.xml中导入依赖,项目全局依赖如下:

      <?xml version="1.0" encoding="UTF-8"?>
      <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
          <modelVersion>4.0.0</modelVersion>
          <groupId>com.javayz</groupId>
          <artifactId>nacos-discovery-spring-cloud-gateway</artifactId>
          <version>0.0.1-SNAPSHOT</version>
          <name>nacos-discovery-spring-cloud-gateway</name>
          <description>Demo project for Spring Boot</description>
          <properties>
              <java.version>1.8</java.version>
              <projec.build.sourceEncoding>UTF-8</projec.build.sourceEncoding>
              <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
              <spring-boot.version>2.3.0.RELEASE</spring-boot.version>
              <spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.version>
          </properties>
       
          <dependencies>
              <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-actuator</artifactId>
              </dependency>
       
              <dependency>
                  <groupId>com.alibaba.cloud</groupId>
                  <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
              </dependency>
       
              <dependency>
                  <groupId>com.alibaba.cloud</groupId>
                  <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
              </dependency>
       
              <!--springcloudgateway-->
              <dependency>
                  <groupId>org.springframework.cloud</groupId>
                  <artifactId>spring-cloud-starter-gateway</artifactId>
              </dependency>
              <dependency>
                  <groupId>org.projectlombok</groupId>
                  <artifactId>lombok</artifactId>
              </dependency>
       
              <dependency>
                  <groupId>com.alibaba.csp</groupId>
                  <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
                  <version>1.7.2</version>
              </dependency>
          </dependencies>
       
          <!--当前项目继承的依赖-->
          <dependencyManagement>
              <dependencies>
                  <dependency>
                      <groupId>com.alibaba.cloud</groupId>
                      <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                      <version>${spring-cloud-alibaba.version}</version>
                      <type>pom</type>
                      <scope>import</scope>
                  </dependency>
       
                  <dependency>
                      <groupId>org.springframework.boot</groupId>
                      <artifactId>spring-boot-dependencies</artifactId>
                      <version>${spring-boot.version}</version>
                      <type>pom</type>
                      <scope>import</scope>
                  </dependency>
       
                  <dependency>
                      <groupId>org.springframework.cloud</groupId>
                      <artifactId>spring-cloud-dependencies</artifactId>
                      <version>Hoxton.SR3</version>
                      <type>pom</type>
                      <scope>import</scope>
                  </dependency>
       
              </dependencies>
          </dependencyManagement>
       
          <build>
              <plugins>
                  <plugin>
                      <groupId>org.apache.maven.plugins</groupId>
                      <artifactId>maven-compiler-plugin</artifactId>
                      <configuration>
                          <source>1.8</source>
                          <target>1.8</target>
                          <encoding>UTF-8</encoding>
                      </configuration>
                  </plugin>
                  <plugin>
                      <groupId>org.springframework.boot</groupId>
                      <artifactId>spring-boot-maven-plugin</artifactId>
                  </plugin>
              </plugins>
          </build>
       
      </project>

      2、配置启动类

      创建GatewayApplication类

      @EnableDiscoveryClient
      @SpringBootApplication
      public class NacosDiscoverySpringCloudGatewayApplication {
          public static void main(String[] args) {
              SpringApplication.run(NacosDiscoverySpringCloudGatewayApplication.class, args);
          }
      }

      3、编写配置文件

      这里对配置文件的编写使用.yml格式的配置文件,在resource文件夹下创建applicaiion.yml。这里需要注意的是gateway下routes的配置,首先用id自定义网关;接着定义uri,意思是当访问的是9090端口(即访问的是网关时),就自动把ip和端口号变成该uri;最后写断言predicates,这里的Path=/index/** 指得是自动匹配 /index/**的路径。当请求地址为http://localhost:9090/index/1时,微服务网关会去访问http://nacos-discovery-consumer/index/1,这就是微服务网关调用微服务的功能。

      server:
        port: 9090
      spring:
        application:
          name: nacos-discovery-spring-cloud-gateway
        cloud:
          gateway:
            discovery:
              locator:
                enabled: true  #启用discoveryClient网关集成,实现服务发现
            routes:
              - id: route1
                uri: lb://nacos-discovery-consumer  #lb://表示负载均衡
                predicates:  #判断条件
                  - Path=/test,/index/**

      (三) 路由配置详解

      路由断言中有许多匹配规则,之前只介绍了Path,其实还有许多匹配规则

      predicates:
          #匹配一个时间,在这个时间之后发生的请求才能访问(格式是ZonedDateTime)
          - After=2021-03-14T11:31:08.377+08:00[Asia/Shanghai] 
          #匹配一个时间,在这个时间之前发生的请求才能访问(ZonedDateTime)
          - Before=2021-03-14T11:31:08.377+08:00[Asia/Shanghai]
          #匹配两个时间,在这个时间之间才能访问
          - Between=2021-03-14T11:31:08.377+08:00[Asia/Shanghai],2021-03-14T12:31:08.377+08:00[Asia/Shanghai] #路由断言之间
          #路由断言Cookie匹配,匹配给定名称(如master)或者正则表达式,如下面配置,请求的cookie中必须有token=master才能访问
          - Cookie=token,master
          #根据请求头信息匹配,使用方式和cookie类似
          - Header=XXX,XXX
          #根据host地址匹配
          - Host=**.somehost.org   
          #根据请求方法匹配
          - Method=GET  
          #根据请求路径进行匹配 
          - Path=***   
          #根据参数匹配,如果配置为token,则所有的请求必须要带上token=XXX
         - Query=token
         #根据远程IP匹配
         - RemoteAddr=192.168.1.1/24    
         #权重,第一个参数是组名,第二个参数是权重,两个route下配置不同权重,会根据权重负载访问
         - Weight=group1,8

      定义请求路径时,我们还可以据微服务名称拉取请求路径,下面是两种不同方法定义uri,实现同样的功能。

      uri: http://127.0.0.1:9001  #当访问9090端口时,自动把ip和端口替换为此uri
      uri: lb://userservice #根据微服务名称拉取请求路径

      总体的配置:

      server:
        port: 9090
      spring:
        application:
          name: nacos-discovery-spring-cloud-gateway
        cloud:
          gateway:
            discovery:
              locator:
                enabled: true  #启用discoveryClient网关集成,实现服务发现
            routes:
              - id: route1
                uri: lb://nacos-discovery-consumer  #lb://表示负载均衡
                predicates:  #判断条件
                  - Path=/test,/index/**,/**
       
          nacos:
            discovery:
              server-addr: 192.168.78.128:8848
            username: nacos
            password: nacos
       
          sentinel:
            eager: true
            transport:
              dashboard: localhost:8080

      3.1 自定义断言配置

      自定义断言的实现分为两步,假设我们现在要实现一个自定义的过滤器,规则是必须携带一个token,并且token要等于指定的值。

      1、定义配置类:这里包含在配置文件要传入的对象值

      @Data
      public class TokenConfig {
          private String token;
      }

      2、定义路由断言工厂:首先是命名必须是XXXRoutePredicateFactory,继承 AbstractRoutePredicateFactory

      @Slf4j
      @Component
      public class TokenRoutePredicateFactory extends AbstractRoutePredicateFactory<TokenConfig> {
          public TokenRoutePredicateFactory(){
              super(TokenConfig.class);
          }
          //用来把配置中的值变成一个集合
          @Override
          public List<String> shortcutFieldOrder() {
              return Collections.singletonList("token");
          }
          //通过jdk1.8的断言接口来返回true或者false,如果是true表示过滤器不拦截
          @Override
          public Predicate<ServerWebExchange> apply(TokenConfig config) {
              return exchange ->{
                  //获取request请求参数
                  MultiValueMap<String,String> valueMap=exchange.getRequest().getQueryParams();
                  boolean flag=false;
                  List<String> list=new ArrayList<>();
                  //将request请求的value保存到集合里
                  valueMap.forEach((k,v)->{
                      list.addAll(v);
                  });
                  //判断有没有和配置文件中相同的值
                  for (String s:list){
                      log.info("Token"+s);
                      if (StringUtils.equalsIgnoreCase(s,config.getToken())){
                          flag=true;
                          break;
                      }
                  }
                  return flag;
              };
          }
      }

      最后在断言处写上我们自定义的内容:

      predicates:  #判断条件
        - Path=/test,/index/**,/**
        - Token=javayz

      只有链接是http://localhost:9090/sentinel/test1?token=javayz带上token=javayz的请求才能访问。

      3.2 断言不匹配404页面自定义

      SpringCloudGateway默认给出的404页面是

      基于SpringCloudGateway实现微服务网关的方式

      这样的页面实在不友好,我们可以通过自定义的方式让他返回一个JSON字符串

      public class MyErrorWebExceptionHandler extends DefaultErrorWebExceptionHandler {
       
          public MyErrorWebExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties, ErrorProperties errorProperties, ApplicationContext applicationContext) {
              super(errorAttributes, resourceProperties, errorProperties, applicationContext);
          }
       
          /**
           * 指定响应处理方法为JSON处理
           * @param errorAttributes
           * @return
           */
          @Override
          protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
              return RouterFunctions.route(RequestPredicates.all(),this::renderErrorResponse);
          }
       
       
          /**
           * 根据code获取对应的HttpStatus
           * @param errorAttributes
           * @return
           */
          @Override
          protected int getHttpStatus(Map<String, Object> errorAttributes) {
              return (int) errorAttributes.get("status");
          }
       
          /**
           * 构建异常信息
           * @param request
           * @param ex
           * @return
           */
          private String buildMessage(ServerRequest request,Throwable ex){
              StringBuilder builder = new StringBuilder("Failed to handle request:");
              builder.append(request.methodName());
              builder.append(" ");
              builder.append(request.uri());
              if (ex!=null){
                  builder.append(ex.getMessage());
              }
              return builder.toString();
          }
       
          /**
           * 返回的json数据
           * @param status
           * @param errorMsg
           * @param data
           * @return
           */
          public static Map<String,Object> response(int status,String errorMsg,Object data){
              Map<String,Object> map=new HashMap<>();
              map.put("code",status);
              map.put("message",errorMsg);
              map.put("data",data);
              return map;
          }
      }

      新建一个类叫做MyErrorWebExceptionHandler:

      再编写一个配置类,将自定义的异常页面代码注入到Bean容器中:

      @Configuration
      public class GatewayConfiguration {
          private final ServerProperties serverProperties;
          private final ApplicationContext applicationContext;
          private final ResourceProperties resourceProperties;
          private final List<ViewResolver> viewResolvers;
          private final ServerCodecConfigurer serverCodecConfigurer;
       
          public GatewayConfiguration(ServerProperties serverProperties, ApplicationContext applicationContext, ResourceProperties resourceProperties, ObjectProvider<List<ViewResolver>> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer) {
              this.serverProperties = serverProperties;
              this.applicationContext = applicationContext;
              this.resourceProperties = resourceProperties;
              this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
              this.serverCodecConfigurer = serverCodecConfigurer;
          }
       
          @Bean("myErrorWebExceptionHandler")
          @Order(Ordered.HIGHEST_PRECEDENCE)
          public ErrorWebExceptionHandler myErrorWebExceptionHandler(ErrorAttributes errorAttributes){
              MyErrorWebExceptionHandler exceptionHandler=new MyErrorWebExceptionHandler(
                      errorAttributes,
                      this.resourceProperties,
                      this.serverProperties.getError(),
                      this.applicationContext
              );
              exceptionHandler.setViewResolvers(this.viewResolvers);
              exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters());
              exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders());
              return exceptionHandler;
          }
      }

      最后访问到404就会以JSON的格式返回,方便调用方做处理。

      基于SpringCloudGateway实现微服务网关的方式

      (四)Spring Cloud Gateway过滤器

      Spring Cloud Gateway除了具有请求路由功能之外,也支持对请求的过滤。

      生命周期:

      过滤器有两种生命周期,PRE和POST:

      PRE:这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。

      POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。

      过滤器类型:

      GatewayFilter:应用到单个路由或者一个分组的路由上

      GlobalFilter:应用到所有的路由上

      局部过滤器可以使用Spring Cloud Gateway内置的方法,在官网中(docs.spring.io/spring-clou…

      基于SpringCloudGateway实现微服务网关的方式

      官方过滤器:

      这里简单介绍几个:

      spring:
        cloud:
          gateway:
            routes:
            - id: add_request_header_route
              uri: https://example.org
              filters:
              - AddRequestHeader=X-Request-red, blue

      添加AddRequestHeader过滤器后,每次请求转发给微服务后都会在header种增加一个X-Request-red。

      – AddRequestParameter=red, blue

      添加Request参数,每次请求转发给微服务后都会增加一个请求参数。

      自定义全局过滤器: 创建一个Filter包,在包下新建GatewayFilter类,自定义全局过滤器需要继承GlobalFilter, Ordered接口,并重写接口的两个方法

      @Component
      public class GatewayFilter implements GlobalFilter, Ordered {
          //在里面写过滤器的逻辑
          @Override
          public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
              System.out.println("进入了过滤器");
              //表示继续向下执行
              return chain.filter(exchange);
          }
          //过滤器的优先级,数值越小,优先级越高
          @Override
          public int getOrder() {
              return 0;
          }
      }

      通过访问可以看到控制台上出现了打印的信息。

      模拟用户认证过程:

      全局过滤器可以实现对全局的用户验证。修改gatewayFilter,编写失败的逻辑,如果获取到的request请求中第一个属性不是access-token或为空,则返回HttpStatus.UNAUTHORIZED的报错

      @Component
      public class GatewayFilter implements GlobalFilter, Ordered {
          //在里面写过滤器的逻辑
          @Override
          public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
              System.out.println("进入了过滤器");
              String token = exchange.getRequest().getQueryParams().getFirst("access-token");
              if (token==null){
                  //如果不存在,认证失败
                  System.out.println("没有登陆");
                  exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
                  return exchange.getResponse().setComplete();
              }
              return chain.filter(exchange);
          }
          //过滤器的优先级,数值越小,优先级越高
          @Override
          public int getOrder() {
              return 0;
          }
      }

      在浏览器中输入:http://localhost:9090/sentinel/test1,出现报错,因为没有access-token

      在浏览器中输入:http://localhost:9090/sentinel/test1?access-token=1,获取正确结果。

      (五) 网关限流

      在实际环境中会遇到这样的问题,同一时间会有许多请求去调用微服务,大量请求的进入很有可能导致微服务被破环,因此限流就变得很重要。

      5.1 常见的一些限流算法:

      计数器算法:设定一个单位时间内的计数值,如果单位时间内访问的请求大于这个设定的值,则之后的请求都拒绝。直到单位时间内的值低于这个值才继续接收。

      漏桶算法:我们可以把漏桶算法想象成一个漏斗。请求进来时先到漏斗中,然后按设定好的输出速率分配到微服务上。当一下子有很多请求进来时,只能在漏斗上等着。为了控制流量,需要设置两个变量:一个是桶的大小,另一个是漏斗流出的速率。

      令牌桶算法:令牌桶算法是对漏桶算法的一种改进,相比于漏桶算法,令牌桶算法能允许一定程度的突发调用。令牌桶算法的原理:在令牌桶算法中,存在一个桶,用来存放固定数量的令牌,令牌会以一定速率放到桶中,桶中的令牌满了之后就不会再放令牌。当一个请求来临时,要先在令牌桶中拿到一个令牌,才能去调用微服务,当令牌桶中没有令牌的时候,后来到的请求就需要等待。令牌桶算法除了可以限流,还允许一定程度的突发调用:比如一个令牌桶的容量是100,在没有请求的时候,令牌桶中的令牌数量是满的。此时如果突然来了100个请求,那么这100个请求都能被立刻执行。

      5.2 集成Sentinel进行限流

      添加依赖

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

      添加Sentinel配置:

      spring:
        cloud:
          sentinel:
            eager: true
            transport:
              dashboard: localhost:8080

      将Sentinel过滤器注入到Bean容器中

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

      这样就可以用Sentinel的方式对网关进行限流了。

      对于限流后的错误提示,我们可以修改默认错误提示方式,改成自定义或者跳转到错误页面:

      @Configuration
      public class GatewayConfiguration {
          private final ServerProperties serverProperties;
          private final ApplicationContext applicationContext;
          private final ResourceProperties resourceProperties;
          private final List<ViewResolver> viewResolvers;
          private final ServerCodecConfigurer serverCodecConfigurer;
       
          public GatewayConfiguration(ServerProperties serverProperties, ApplicationContext applicationContext, ResourceProperties resourceProperties, ObjectProvider<List<ViewResolver>> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer) {
              this.serverProperties = serverProperties;
              this.applicationContext = applicationContext;
              this.resourceProperties = resourceProperties;
              this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
              this.serverCodecConfigurer = serverCodecConfigurer;
          }
       
          //自定义一个错误处理
          @Bean(name = "myBlockRequestHandler")
          public BlockRequestHandler myBlockRequestHandler(){
              BlockRequestHandler blockRequestHandler=new BlockRequestHandler() {
                  @Override
                  public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
                      return ServerResponse.status(HttpStatus.BAD_GATEWAY)
                              .contentType(MediaType.APPLICATION_JSON)
                              .body(BodyInserters.fromValue("服务被限流了"));
                  }
              };
              return blockRequestHandler;
          }
       
          //用自定义的handler替换掉默认的,或者重定向
          @Bean
          @Order(Ordered.HIGHEST_PRECEDENCE)
          public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler(BlockRequestHandler myBlockRequestHandler){
              //自定义Block处理
              GatewayCallbackManager.setBlockHandler(myBlockRequestHandler);
       
              //重定向block处理
              //GatewayCallbackManager.setBlockHandler(new RedirectBlockRequestHandler("http://www.baidu.com"));
              return new SentinelGatewayBlockExceptionHandler(viewResolvers,serverCodecConfigurer);
          }
       
          @Bean
          @Order(-1)
          public GlobalFilter sentinelGatewayFilter(){
              return new SentinelGatewayFilter();
          }
      }

      5.3 网关实现跨域

      这个和SpringBoot中解决跨域的思路类似,配置CorsConfig

      @Configuration
      public class CorsConfig {
       
          @Bean
          public CorsWebFilter corsWebFilter(){
              CorsConfiguration configuration=new CorsConfiguration();
              configuration.addAllowedHeader("*");
              configuration.addAllowedMethod("*");
              configuration.addAllowedOrigin("*");
       
              UrlBasedCorsConfigurationSource source=new UrlBasedCorsConfigurationSource(new PathPatternParser());
              source.registerCorsConfiguration("/*",configuration);
              return new CorsWebFilter(source);
          }
      }

      (六)总结

      本章内容比较多,但是把SpringCloudGateway基本会遇到的内容都讲解了一遍,希望对你有帮助。

      声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。