前言:在微服务架构中,有一个组件可以说是必不可少的,那就是微服务网关。微服务网关处理了路由转发,负载均衡,缓存,权限校验,监控,限流控制,日志等。Spring Cloud Gateway是Spring Cloud官方推出的第二代网关框架,目标是取代 Netflix Zuul 网关。它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全、监控、埋点和限流等。

一、API 网关简介

服务网关可以这样简单理解:服务网关 = 路由转发 + 过滤器

1、路由转发:接收一切外界请求,转发到后端的微服务上去;

2、过滤器:在服务网关中可以完成一系列的横切功能,例如权限校验、限流以及监控等,这些都可以通过过滤器完成(其实路由转发也是通过过滤器实现的)。

1.1、为什么要用微服务网关

①API 网关出现的原因是微服务架构的出现,不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信,会有以下的问题:

(1)客户端会维护成百上千个微服务URL地址,增加了客户端的复杂性

(2)存在跨域请求,在一定场景下处理相对复杂。

(3)认证复杂,每个服务都需要独立认证。

(4)难以重构,随着项目的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分成多个。如果客户端直接与微服务通信,那么重构将会很难实施。

(5)某些微服务可能使用了防火墙 / 浏览器不友好的协议,直接访问会有一定的困难。

以上这些问题可以借助 API 网关解决。API 网关是介于客户端和服务器端之间的中间层,所有的外部请求都会先经过 API 网关这一层。也就是说,API 的实现方面更多的考虑业务逻辑,而安全、性能、监控可以交由 API 网关来做,这样既提高业务灵活性又不缺安全性。

②而且 API网关 是任何微服务架构的重要组成部分。有了它我们可以在一个独立的模块上方便的处理一些非业务逻辑,可以让微服务本身专注在自身特定的功能上,使得每个微服务的开发更容易和更快速。

其实在我们的项目曾经还是单体应用的时候,虽然没有「 API网关 」的概念,但是一般在项目中都会用到filter过滤器之类的东西,filter的作用就是把项目中的一些非业务逻辑的功能抽离出来独立处理,避免与业务逻辑混在一起增加代码复杂度。比如 鉴权认证功能、Session处理、安全检查、日志处理等等。

现在我们采用微服务架构了,在一个项目中微服务节点很多,如果让每一个节点都去处理上面这些 “鉴权认证功能、Session处理、安全检查、日志处理等” 会多出很多冗余的代码,也会给增加业务代码的复杂度,因此我们就需要有一个「 API网关 」把这些公共的功能独立出来成为一个服务来统一的处理这些事情。

我个人理解API 网关就是一个增强版业务Nginx,它支持请求转发,负载均衡,统一埋点,限流降级、安全认证等等很多功能,Gateway网关收集所有请求根据路由规则转发请求,使用统一的过滤器处理请求参数等,是不是听起来有点像Nginx,但是比Nginx拥有更多功能。

1.2、API网关主要功能

API网关就像是微服务的大门守卫一样,是连通外部客户端与内部微服务之间的一个桥梁。

其主要功能有:

  • 路由转发

    之前说了「API网关」是内部微服务的对外唯一入口,所以外面全部的请求都会先发到这个「API网关」上,然后由「API网关」来根据不同的请求去路由到不同的微服务节点上。例如可以 根据路径 来转发、也可以 根据参数来转发。

    并且由于内部微服务实例也会随着业务调整不停的变更,增加或者删除节点,「API网关」可以与「服务注册」模块进行协同工作,保证将外部请求转发到最合适的微服务实例上面去。

  • 负载均衡

    既然「API网关」是内部微服务的单一入口,所以「API网关」在收到外部请求之后,还可以根据内部微服务每个实例的负荷情况进行动态的负载均衡调节。一旦内部的某个微服务实例负载很高,甚至是不能及时响应,则「API网关」就通过负载均衡策略减少或停止向这个实例转发请求。当所有的内部微服务实例都处理不过来的时候,「API网关」还可以采用限流或熔断的形式阻止外部请求,以保障整个系统的可用性。

  • 安全认证

    「API网关」就像是微服务的大门守卫,每一个请求进来之后,都必须先在「API网关」上进行身份验证,身份验证通过后才转发给后面的服务,转发的时候一般也会带上身份信息。

    同时「API网关」也需要对每一个请求进行安全性检查,例如参数的安全性、传输的安全性等等。

  • 日志记录

    既然所有的请求都需要走「API网关」,那么我们就可以在「API网关」上统一集中的记录下这些行为日志。这些日志既可以作为我们后续事件查询使用,也可以作为系统的性能监控使用。

  • 数据转换

    因为「API网关」对外是面向多种不同的客户端,不同的客户端所传输的数据类型可能是不一样的。因此「API网关」还需要具备数据转换的功能,将不同客户端传输进来的数据转换成同一种类型再转发给内部微服务上,这样,兼容了这些请求的多样性,保证了微服务的灵活性。


 二、Spring Cloud Gateway简介

2.1、Spring Cloud Gateway概述

Spring Cloud Gateway是Spring Cloud官方推出的第二代网关框架,目标是取代 Netflix Zuul 网关。该项目是基于Netty、Reactor以及WebFlux构建,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全、监控、埋点和限流等。

优点

  • 性能强劲,是Zuul的1.6倍

  • 功能强大,内置了很多实用的功能,例如转发、监控、限流等

  • 设计优雅,容易扩展

缺点

  • 依赖Netty与WebFlux,不是传统的Servlet编程模型,有一定的学习成本

  • 不能在Servlet容器下工作,也不能构建成WAR包,即不能将其部署在Tomcat、Jetty等Servlet容器里,只能打成jar包执行

  • 不支持Spring Boot 1.x,需2.0及更高的版本

2.2、Spring Cloud Gateway核心概念

网关的核心概念就是路由配置和路由规则,而作为所有请求流量的入口,在实际生产环境中为了保证高可靠和高可用,是尽量要避免重启的,所以实现动态路由是非常有必要的。

GateWay的主要功能之一是转发请求,转发规则的定义主要包含三个部分:

Route(路由)

这是Spring Cloud Gateway的基本构建块,可简单理解成一条转发规则。包含:ID、目标URL、一组断言和一组过滤器

Predicate(断言)

这是一个 Java 8 的 Predicate,即java.util.function.Predicate这个接口,Gateway使用Predicate实现路由的匹配条件。断言,路径相匹配的进行路由。

Filter(过滤器)

一个标准的Spring WebFilter。Spring cloud gateway中的filter分为两种类型
的Filter,分别是Gateway Filter和Global Filter。过滤器Filter将会对请求和响应进行修改处理。

Gateway路由配置示例

spring:
  cloud:
    gateway:
      routes:
      - id: order-service  # 路由唯一标识,没有固定规则但要求唯一,建议使用服务名
        #匹配后提供服务的目标路由地址
        uri: lb://order-service  # 目标URL,lb代表从注册中心获取服务,lb是Load Balance的缩写
        predicates:
        # Predicate集合
        - Path=/findBy  # 匹配转发路径:断言,路径相匹配的进行路由
        filters:
        # Filter集合
        - StripPrefix=4  # 从第几级开始转发

#配置示例
spring:
  application:
    name: gateway-service
  cloud:
    gateway:
      routes:
        - id: data-service1  #请求 http://localhost:8100/data-service1/test会转发到data-producer服务
          uri: lb://data-producer  #在服务注册中心找服务名为 data-producer的服务
          predicates:
            - Path=/data-service1/*  #设置路由断言,代理servicerId为data-service1的/ data-service1 /路径
          filters:
            - StripPrefix=1
        - id: data-service2  # 请求 http://localhost:8100/data-service2/test转发到 http://localhost:8080/test
          uri: http://localhost:8080
          predicates:
            - Path=/data-service2/*
          filters:
            - StripPrefix=1  

 2.3、Gateway转发规则

${GATEWAY_URL}/{微服务名}/{请求路径}

2.4、Spring Cloud Gateway工作流程


三、Spring Cloud Alibaba整合Spring Cloud Gateway

Geteway网关的核心功能是:过滤和路由转发

 3.1、新建api-gateway子Moudle

3.2、添加Spring Cloud Gateway依赖

网关组件一般都配合服务发现组件使用,我这里使用Nacos作为服务发现组件,具体的依赖如下:

       <!--gateway依赖 -->
       <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
            <version>${spring-cloud-alibaba.version}</version>
        </dependency>

        <!--nacos依赖 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

注意:Spring cloud gateway是基于webflux的,如果非要web支持的话需要导入spring-boot-starter-webflux而不是spring-boot-start-web。Gateway组件已经依赖了 starter-webflux,所以这里 千万不要依赖spring-boot-start-web

3.3 、修改配置文件application.yml文件

server:
  port: 8001 #服务端口

spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848 #nacos服务地址
    gateway:
      discovery:
        locator:
          enabled: true #使用服务发现路由(开启从注册中心动态创建路由的功能,利用微服务名进行路由)
      routes:
        - id: my-service #设置路由id
          uri: lb://order-service #设置路由的url  lb://nacos服务注册名称
          predicates:
            - Path=/api/order/* #路径匹配规则

3.4 、应用启动类添加注解 @EnableDiscoveryClient

 3.5  启动GateWay网关服务,访问测试

通过服务名访问是可以的:http://localhost:8002/order-service/order

通过网关断言方式也可以:http://localhost:8002/api/order

 

3.6、解决跨域问题

package com.caiweiwei.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;

@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);
    }
}

需要注意的是,这个与@CrossOrigin不能同时存在

3.7、全局Filter,统一处理会员登录与外部不允许访问的服务

package com.caiweiwei.filter;

import com.google.gson.JsonObject;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.util.List;

public class AuthGlobalFilter implements GlobalFilter, Ordered {
    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getURI().getPath();
        //谷粒学院api接口,校验用户必须登录
        if (antPathMatcher.match("/api/**/auth/**", path)) {
            List<String> tokenList = request.getHeaders().get("token");
            if (null == tokenList) {
                ServerHttpResponse response = exchange.getResponse();
                return out(response);
            } else {
        // Boolean isCheck = JwtUtils.checkToken(tokenList.get(0));
        // if(!isCheck) {
                ServerHttpResponse response = exchange.getResponse();
                return out(response);
        // }
            }
        }
        //内部服务接口,不允许外部访问
        if (antPathMatcher.match("/**/inner/**", path)) {
            ServerHttpResponse response = exchange.getResponse();
            return out(response);
        }
        return chain.filter(exchange);
    }

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

    private Mono<Void> out(ServerHttpResponse response) {
        JsonObject message = new JsonObject();
        message.addProperty("success", false);
        message.addProperty("code", 28004);
        message.addProperty("data", "鉴权失败");
        byte[] bits = message.toString().getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = response.bufferFactory().wrap(bits);
        //response.setStatusCode(HttpStatus.UNAUTHORIZED);
        //指定编码,否则在浏览器中会中文乱码
        response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        return response.writeWith(Mono.just(buffer));

    }
}

3.8、自定义异常处理

服务网关调用服务时可能会有一些异常或服务不可用,它返回错误信息不友好,需要我们覆盖处理
ErrorHandlerConfig:

package com.caiweiwei.handler;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.web.ResourceProperties;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.result.view.ViewResolver;
import java.util.Collections;
import java.util.List;

/**
 * 覆盖默认的异常处理
 *
 */
@Configuration
@EnableConfigurationProperties({ServerProperties.class, ResourceProperties.class})
public class ErrorHandlerConfig {
    private final ServerProperties serverProperties;
    private final ApplicationContext applicationContext;
    private final ResourceProperties resourceProperties;
    private final List<ViewResolver> viewResolvers;
    private final ServerCodecConfigurer serverCodecConfigurer;
    public ErrorHandlerConfig(ServerProperties serverProperties,
                              ResourceProperties resourceProperties,
                              ObjectProvider<List<ViewResolver>>
                                      viewResolversProvider,
                              ServerCodecConfigurer
                                      serverCodecConfigurer,
                              ApplicationContext applicationContext) {
        this.serverProperties = serverProperties;
        this.applicationContext = applicationContext;
        this.resourceProperties = resourceProperties;
        this.viewResolvers =
                viewResolversProvider.getIfAvailable(Collections::emptyList);
        this.serverCodecConfigurer = serverCodecConfigurer;
    }
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes) {
        JsonExceptionHandler exceptionHandler = new JsonExceptionHandler(
                errorAttributes,
                this.resourceProperties,
                this.serverProperties.getError(),
                this.applicationContext);
        exceptionHandler.setViewResolvers(this.viewResolvers);
        exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters());
        exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders());
        return exceptionHandler;
    }
}

JsonExceptionHandler:

package com.caiweiwei.handler;

import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.autoconfigure.web.ResourceProperties;
import org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.context.ApplicationContext;
import org.springframework.web.reactive.function.server.*;

import java.util.HashMap;
import java.util.Map;

public class JsonExceptionHandler extends DefaultErrorWebExceptionHandler {
    public JsonExceptionHandler(ErrorAttributes errorAttributes,
                                ResourceProperties resourceProperties,
                                ErrorProperties errorProperties,
                                ApplicationContext applicationContext) {
        super(errorAttributes, resourceProperties, errorProperties,
                applicationContext);
    }

    /**
     * 获取异常属性
     */
    @Override
    protected Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
        Map<String, Object> map = new HashMap<>();
        map.put("success", false);
        map.put("code", 20005);
        map.put("message", "网关失败");
        map.put("data", null);
        return map;
    }

    /**
     * 指定响应处理方法为JSON处理的方法
     *
     * @param errorAttributes
     */
    @Override
    protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
        return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
    }

    /**
     * 根据code获取对应的HttpStatus
     *
     * @param errorAttributes
     */
    @Override
    protected int getHttpStatus(Map<String, Object> errorAttributes) {
        return 200;
    }

}

参考链接:

若依微服务SpringCloud—使用Spring Cloud Gateway网关-pudn.com

微服务中网关的作用及搭建

从零搭建Spring-Cloud-Alibaba(五)- Gateway网关

Gateway网关服务搭建和配置



 

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐