从零开始的Spring Cloud Gateway指南:构建强大微服务架构

目录

  • [一、 什么是Gateway?](#一、 什么是Gateway?)
    • [1. 网关的由来](#1. 网关的由来)
    • [2. 网关的作用](#2. 网关的作用)
    • [3. 网关的技术实现](#3. 网关的技术实现)
  • 二、如何搭建一个简易网关服务
    • [1. 引入依赖](#1. 引入依赖)
    • [2. 配置yml文件](#2. 配置yml文件)
  • 三、进阶话题:过滤器和路由配置
    • [1. gateway的执行原理](#1. gateway的执行原理)
    • [2. 路由断言工厂: Predicate Factory](#2. 路由断言工厂: Predicate Factory)
    • [3. 网关过滤器:GatewayFilter](#3. 网关过滤器:GatewayFilter)
    • [4. 默认过滤器:default Filter](#4. 默认过滤器:default Filter)
    • [5. 全局过滤器:GlobalFilter](#5. 全局过滤器:GlobalFilter)
    • [6. 过滤器的执行顺序](#6. 过滤器的执行顺序)
    • [7. GlobalFilter和GatewayFilter的区别](#7. GlobalFilter和GatewayFilter的区别)
  • 四、实战经验分享:处理微服务间通信
    • [1. 跨域问题处理](#1. 跨域问题处理)

微服务架构的兴起已经改变了软件开发的面貌,使得开发者能够更灵活地构建、部署和维护应用程序。而在这个微服务的时代,强大而灵活的网关是确保微服务之间通信顺畅的关键之一。在本文中,我们将深入研究Spring Cloud Gateway,一款开源的、基于Spring Framework的微服务网关,带领你从零开始构建强大的微服务架构。

一、 什么是Gateway?

1. 网关的由来

单体应用拆分成多个服务后,对外需要一个统一入口,解耦客户端与内部服务
注:图片来自网络

2. 网关的作用

Spring Cloud Gateway是Spring Cloud生态系统中的一员,它被设计用于处理所有微服务的入口流量。作为一个反向代理,它不仅提供了负载均衡和路由功能,还支持灵活的过滤器机制,过滤器可以在请求进入网关和离开网关时执行,用于处理各种逻辑,如身份验证、日志记录和性能监测,使得开发者能够定制和扩展其功能。

网关核心功能是路由转发,因此不要有耗时操作在网关上处理,让请求快速转发到后端服务上。

注:图片来自网络

3. 网关的技术实现

在SpringCloud中网关的实现包括两种:

  • gateway
  • zuul

Zuul 是基于Servlet的实现,在1.x版本属于阻塞式编程,在2.x后是基于netty,是非阻塞的。而SpringCloudGateway则是基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能。

目前一般都选用SpringCloudGateway,毕竟都是Spring家族一员,支持性更好。

二、如何搭建一个简易网关服务

1. 引入依赖

需要先搭建一个SpringBoot项目,在父pom中引入依赖

xml 复制代码
<properties>
        <spring-cloud.version>2021.0.1</spring-cloud.version>
        <spring-cloud-alibaba.version>2021.0.1.0</spring-cloud-alibaba.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <!-- SpringCloud 微服务 -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!-- SpringCloud Alibaba 微服务 -->
            <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>

    </dependencyManagement>

在gateway服务中引入相关依赖

xml 复制代码
<dependencies>
        <!--spring cloud gateway-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!--nacos 服务发现-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

    </dependencies>

此外,请检查一下你的依赖中是否含有spring-boot-starter-web,如果有, 请干掉它 。因为我们的SpringCloud Gateway是一个netty+webflux实现的web服务器,和Springboot Web本身就是冲突的。

2. 配置yml文件

xml 复制代码
server:
  port: 8081 # 网关端口
spring:
  application:
    name: gateway # 服务名称
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848 # nacos地址
    gateway:
      routes: # 网关路由配置
        - id: baidu # 路由id, 自定义,唯一即可
          # uri: 127.0.0.1:/order # - 路由目的地,支持lb和http两种
          uri: lb://orderService # 路由的目的地,lb是负载均衡,后面跟服务名称
          predicates: # 路由断言,也就是判断是否符合路由规则的条件
            - Path=/order-service/** # path 按照路径进行匹配,只要以/order-service/开头就符合规则
          filters:
            - StripPrefix=1 # 过滤器,去除请求的前缀路径,StripPrefix=1 表示去除请求路径的第一个路径片段

网关路由可以配置的内容包括:

  • 路由id:路由唯一标示
  • uri: 路由目的地,支持lb和http两种
  • predicates: 路由断言,判断请求是否符合要求,符合则转发到路由目的地
  • filters:路由过滤器,处理请求或响应

做到这里,一个简易的路由转发网关服务就搭建成功了,直接运行GatewayApplication服务,得到结果如图:

接下来在网页直接访问:http://127.0.0.1:8081/order-service/xx,网关就会将路由转发到orderService服务。

如果你是通过负载均衡路由到其他服务,这时候可能会报问题:

Whitelabel Error Page

This application has no configured error view, so you are seeing this as a fallback.

这是由于缺少一个相关的依赖

xml 复制代码
<!--客户端负载均衡loadbalancer-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

原因:

由于springcloud2020弃用了Ribbon,因此Alibaba在2021版本nacos中删除了Ribbon的jar包,因此无法通过lb路由到指定微服务,出现了503情况。

所以只需要引入springcloud loadbalancer包即可

引入依赖重新运行,OK!

三、进阶话题:过滤器和路由配置

网关服务一个主要作用就是路由配置,通过不同的路由配置实现相应的功能。

1)Route:路由是网关的基本构件 。它由ID、目标URI、路由断言工厂(谓语动词)和过滤器集合定义。如果断言工厂判断为真,则匹配路由。

2)Predicate:路由断言工厂,参照Java8的新特性Predicate 。这允许开发人员匹配HTTP请求中的任何内容,比如请求头或参数。

3)Filter:路由过滤器。可以在发送下游请求之前或之后修改请求和响应。

下面看一个案例:

xml 复制代码
spring:
  application:
    name: gateway # 服务名称
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848 # nacos地址
    gateway:
      routes: # 网关路由配置
        - id: order# 路由id, 自定义,唯一即可
          # uri: 127.0.0.1:/order # - 路由目的地,支持lb和http两种
          uri: lb://orderService # 路由的目的地,lb是负载均衡,后面跟服务名称
          predicates: # 路由断言,也就是判断是否符合路由规则的条件
            - Path=/order-service/** # path 按照路径进行匹配,只要以/order-service/开头就符合规则
          filters: # 过滤器,对请求和响应进行处理
            - StripPrefix=1 # 过滤器,去除请求的前缀路径,StripPrefix=1 表示去除请求路径的第一个路径片段


注:图片来自网络

1. gateway的执行原理

网关的核心逻辑就是路由转发,执行过滤器链。在下面的处理过程中,Gateway Handler Mapping将请求和路由进行匹配,这时候就需要用到predicate,它是决定了一个请求是否走哪一个路由。

客户端向 Spring Cloud Gateway 发出请求。然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。Handler 再通过指 定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前("pre")或之后("post")执行业务逻辑。

Filter在"pre"类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等,在"post"类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用。

2. 路由断言工厂: Predicate Factory

我们在配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转变为路由判断的条件。

例如Path=/user/**是按照路径匹配,这个规则是由org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory类来处理的。

像这样的断言工厂在SpringCloudGateway还有十几个:

在上图中,有很多类型的Predicate。

比如说时间类型的Predicated(AfterRoutePredicateFactory、 BeforeRoutePredicateFactory、BetweenRoutePredicateFactory),当只有满足特定时间要求的请求才会交由router处理;

xml 复制代码
spring:
  cloud:
    gateway:
      routes: # 网关路由配置
        - id: order
          uri: lb://orderService 
          predicates: 
            - Before=2023-12-25T14:33:47.789+08:00

这个路由规则会在东8区的2023-12-25 14:33:47后,将请求转跳到订单服务。

cookie类型的CookieRoutePredicateFactory,cookie名字和正则表达式的value作为两个输入参数,请求的cookie需要匹配cookie名和符合其中value的正则,才会进入此router;

xml 复制代码
spring:
  cloud:
    gateway:
      routes: # 网关路由配置
        - id: order
          uri: lb://orderService 
          predicates: 
            - Cookie=cookiename, cookievalue

路由匹配请求存在cookie名为cookiename,cookie内容匹配cookievalue的,将请求转发到订单服务。

以及host、method、path、querparam、remoteaddr类型的predicate,每一种predicate都会对当前的客户端请求进行判断,是否满足当前的要求,如果满足则交给当前请求处理。

如果有很多个Predicate,并且一个请求满足多个Predicate,则按照配置的顺序第一个生效。
请注意:一个请求满足多个路由的谓词条件时,请求只会被首个成功匹配的路由转发。

3. 网关过滤器:GatewayFilter

GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理

针对某些特殊的场景,Spring Cloud Gateway提供了31种不同的路由过滤器工厂。例如



注:图片来自网络

案例:

  1. AddRequestHeader:给所有进入orderService的请求添加一个请求头: mark=specifications
xml 复制代码
spring:
  cloud:
    gateway:
      routes: # 网关路由配置
        - id: order
          uri: lb://orderService 
          filters:
            - AddRequestHeader=mark, specifications

对匹配的请求,会额外添加mark:specifications的header。

AddRequestParameter、AddResponseHeader 类似。

  1. RemoveRequestHeader:移除请求的header
xml 复制代码
spring:
  cloud:
    gateway:
      routes: # 网关路由配置
        - id: order
          uri: lb://orderService 
          filters:
            - RemoveRequestHeader=mark

上面路由在发送请求给订单服务时,会将请求中的mark头信息去掉。

  1. StripPrefix:截断请求路径前缀
xml 复制代码
spring:
  cloud:
    gateway:
      routes: # 网关路由配置
        - id: order
          uri: lb://orderService
          predicates: 
            - Path=/order-service/** 
          filters:
            - StripPrefix=1

StripPrefix GatewayFilter Factory通过配置parts来表示截断路径前缀的数量。上面例子中,如果请求的路径为/order-service/order/1,则路径会修改为/order/1。

gateway提供的过滤器很多,这里不一一做展示。

自定义网关过滤器

定义方式是实现AbstractGatewayFilterFactory<>类。

java 复制代码
public abstract class AbstractGatewayFilterFactory<C> extends AbstractConfigurable<C> implements GatewayFilterFactory<C>, ApplicationEventPublisherAware {
    private ApplicationEventPublisher publisher;

    public AbstractGatewayFilterFactory() {
        super(Object.class);
    }

    public AbstractGatewayFilterFactory(Class<C> configClass) {
        super(configClass);
    }

    protected ApplicationEventPublisher getPublisher() {
        return this.publisher;
    }

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

    public static class NameConfig {
        private String name;

        public NameConfig() {
        }

        public String getName() {
            return this.name;
        }

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

自定义过滤器实现黑名单功能

java 复制代码
@Component
public class BlackListUrlFilter extends AbstractGatewayFilterFactory<BlackListUrlFilter.Config>
{
    @Override
    public GatewayFilter apply(Config config)
    {
        return (exchange, chain) -> {

            String url = exchange.getRequest().getURI().getPath();
            if (config.matchBlacklist(url))
            {
                return ServletUtils.webFluxResponseWriter(exchange.getResponse(), "请求地址不允许访问");
            }

            return chain.filter(exchange);
        };
    }

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

    public static class Config
    {
        private List<String> blacklistUrl;

        private List<Pattern> blacklistUrlPattern = new ArrayList<>();

        public boolean matchBlacklist(String url)
        {
            return !blacklistUrlPattern.isEmpty() && blacklistUrlPattern.stream().anyMatch(p -> p.matcher(url).find());
        }

        public List<String> getBlacklistUrl()
        {
            return blacklistUrl;
        }

        public void setBlacklistUrl(List<String> blacklistUrl)
        {
            this.blacklistUrl = blacklistUrl;
            this.blacklistUrlPattern.clear();
            this.blacklistUrl.forEach(url -> {
                this.blacklistUrlPattern.add(Pattern.compile(url.replaceAll("\\*\\*", "(.*?)"), Pattern.CASE_INSENSITIVE));
            });
        }
    }

}

配置自定义过滤器

xml 复制代码
spring:
  cloud:
    gateway:
      routes: # 网关路由配置
        - id: order
          uri: lb://orderService 
          filters:
            - name: BlackListUrlFilter
              args:
                blacklistUrl:
                  - /user/list
                  - 192.168.20.1

4. 默认过滤器:default Filter

如果要对所有的路由都生效,则可以将过滤器工厂写到default下。格式如下

xml 复制代码
spring:
  cloud:
    gateway:
      routes: # 网关路由配置
        - id: order # 路由id, 自定义,唯一即可
          # uri: http://www.baidu.com # - 路由目的地,支持lb和http两种
          uri: lb://orderService # 路由的目的地,lb是负载均衡,后面跟服务名称
          predicates: # 路由断言,也就是判断是否符合路由规则的条件
            - Path=/order-service/** 
          filters:
            - StripPrefix=1 
            - AddRequestHeader=mark, specifications
      default-filters: 
        - AddRequestHeader=mark, specifications 

default-filters下的过滤器将对所有的路由都生效。

5. 全局过滤器:GlobalFilter

全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样。

区别在于GatewayFilter通过配置定义,处理逻辑是固定的。而GlobalFilter的逻辑需要自己写代码实现。

我们可以通过GlobalFilter实现日志记录、权限校验、流量监控等功能

定义方式是实现GlobalFilter接口:

自定义全局过滤器实现网关鉴权:

java 复制代码
@Component
public class AuthFilter implements GlobalFilter, Ordered {
    private static final Logger log = LoggerFactory.getLogger(AuthFilter.class);

    // 白名单,排除过滤的 uri 地址
    @Autowired
    private IgnoreWhiteProperties ignoreWhite;

    @Autowired
    private RedisService redisService;


    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        ServerHttpRequest.Builder mutate = request.mutate();

        String url = request.getURI().getPath();
        // 跳过不需要验证的路径
        if (StringUtils.matches(url, ignoreWhite.getWhites())) {
            return chain.filter(exchange);
        }
        String token = getToken(request);
        if (StringUtils.isEmpty(token)) {
            token=getWebsocketToken(request,response);
            if (StringUtils.isEmpty(token)){
                return unauthorizedResponse(exchange, "令牌不能为空");
            }
        }
        Claims claims = JwtUtils.parseToken(token);
        if (claims == null) {
            return unauthorizedResponse(exchange, "令牌已过期或验证不正确!");
        }
        String userkey = JwtUtils.getUserKey(claims);
        boolean islogin = redisService.hasKey(getTokenKey(userkey));
        if (!islogin) {
            return unauthorizedResponse(exchange, "登录状态已过期");
        }
        String userid = JwtUtils.getUserId(claims);
        String username = JwtUtils.getUserName(claims);
        if (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username)) {
            return unauthorizedResponse(exchange, "令牌验证失败");
        }
        String nickName = JwtUtils.getNickName(claims);


        // 设置用户信息到请求
        addHeader(mutate, SecurityConstants.USER_KEY, userkey);
        addHeader(mutate, SecurityConstants.DETAILS_USER_ID, userid);
        addHeader(mutate, SecurityConstants.DETAILS_USERNAME, username);
       
        // 内部请求来源参数清除
        removeHeader(mutate, SecurityConstants.FROM_SOURCE);
        return chain.filter(exchange.mutate().request(mutate.build()).build());
    }

    private void addHeader(ServerHttpRequest.Builder mutate, String name, Object value) {
        if (value == null) {
            return;
        }
        String valueStr = value.toString();
        String valueEncode = ServletUtils.urlEncode(valueStr);
        mutate.header(name, valueEncode);
    }

    private void removeHeader(ServerHttpRequest.Builder mutate, String name) {
        mutate.headers(httpHeaders -> httpHeaders.remove(name)).build();
    }

    private Mono<Void> unauthorizedResponse(ServerWebExchange exchange, String msg) {
        log.error("[鉴权异常处理]请求路径:{}", exchange.getRequest().getPath());
        return ServletUtils.webFluxResponseWriter(exchange.getResponse(), msg, HttpStatus.UNAUTHORIZED);
    }

    /**
     * 获取缓存key
     */
    private String getTokenKey(String token) {
        return CacheConstants.LOGIN_TOKEN_KEY + token;
    }

    /**
     * 获取请求token
     */
    private String getToken(ServerHttpRequest request) {
        String token = request.getHeaders().getFirst(TokenConstants.AUTHENTICATION);
        // 如果前端设置了令牌前缀,则裁剪掉前缀
        if (StringUtils.isNotEmpty(token) && token.startsWith(TokenConstants.PREFIX)) {
            token = token.replaceFirst(TokenConstants.PREFIX, StringUtils.EMPTY);
        }
        return token;
    }
    private String getWebsocketToken(ServerHttpRequest request,ServerHttpResponse response){
        String token = request.getHeaders().getFirst(TokenConstants.WEBSOCKET_AUTHENTICATION);
        ServerHttpRequest.Builder mutate = request.mutate();
        // 如果前端设置了令牌前缀,则裁剪掉前缀
        if (StringUtils.isNotEmpty(token) ) {
            response.getHeaders().set(TokenConstants.WEBSOCKET_AUTHENTICATION, token);
            removeHeader(mutate, TokenConstants.WEBSOCKET_AUTHENTICATION);
        }
        return token;
    }

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

6. 过滤器的执行顺序

请求进入网关会碰到三类过滤器: 当前路由的过滤器、DefaultFilter、GlobalFilter。

请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器。

  • 每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前
  • GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值,由我们自己指定
  • 路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增
  • 当过滤器的order值一样时,会按照 defaultFilter > 路由过滤器>GlobalFilter的顺序执行

可以参考下面几个类的源码查看:

7. GlobalFilter和GatewayFilter的区别

Spring Cloud Gateway 根据作用范围划分为 GatewayFilter 和 GlobalFilter,二者区别如下:

  • GatewayFilter:网关过滤器,需要通过 spring.cloud.routes.filters 配置在具体路由下,只作用在当前路由上或通过 spring.cloud.default-filters 配置在全局,作用在所有路由上。
  • GlobalFilter:全局过滤器,不需要在配置文件中配置,作用在所有的路由上,最终通过 GatewayFilterAdapter 包装成 GatewayFilterChain 可识别的过滤器,它为请求业务以及路由的 URI 转换为真实业务服务请求地址的核心过滤器,不需要配置系统初始化时加载,并作用在每个路由上。

四、实战经验分享:处理微服务间通信

1. 跨域问题处理

跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题。它是由浏览器的同源策略所造成的,是浏览器对于JavaScript所定义的安全限制策略。

同源策略:是指协议,域名,端口都要相同,其中有一个不同都会产生跨域。

  • 同一协议, 如http或https
  • 同一IP地址或域名, 如127.0.0.1
  • 同一端口, 如8080

以上三个条件中有一个条件不同就会产生跨域问题。

例如:

解决方案:

跨源资源共享 (CORS):(或通俗地译为跨域资源共享)是一种基于 HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其它origin(域,协议和端口),这样浏览器可以访问加载这些资源。现代浏览器支持在 API 容器中(例如 XMLHttpRequest 或 Fetch)使用 CORS,以降低跨源 HTTP 请求所带来的风险。

网关处理跨域采用的同样是CORS方案,并且只需要简单配置即可实现。

CORS跨域要配置的参数包括:

  • 允许哪些域名跨域?
  • 允许哪些请求头?
  • 允许哪些请求方式?
  • 是否允许使用cookie?
  • 有效期是多久?
xml 复制代码
spring: 
  cloud:
    gateway:
      globalcors: # 全局跨域处理
      	add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
        cors-configurations:
          '[/**]':
            # 允许跨域的源(网站域名/ip),设置*为全部
            # 允许跨域请求里的head字段,设置*为全部
            # 允许跨域的method, 默认为GET和OPTIONS,设置*为全部
            allow-credentials: true # 是否允许携带cookie
            allowed-origins: # 运行的跨域请求的网站
              - "http://taobao.123.com"
              - "http://www.xx.com"
            allowed-headers: "*" # 允许在请求中携带的头信息
            allowed-methods: # 允许跨域ajax请求方式
              - OPTIONS
              - GET
              - POST
              - DELETE
              - PUT
              - PATCH
            max-age: 36000  # 跨域检测的有效期
相关推荐
周湘zx33 分钟前
k8s中的微服务
linux·运维·服务器·微服务·云原生·kubernetes
爱上语文5 小时前
Springboot三层架构
java·开发语言·spring boot·spring·架构
你知道“铁甲小宝”吗丶6 小时前
【第33章】Spring Cloud之SkyWalking服务链路追踪
java·spring boot·spring·spring cloud·skywalking
ღ᭄ꦿ࿐Never say never꧂7 小时前
微服务架构中的负载均衡与服务注册中心(Nacos)
java·spring boot·后端·spring cloud·微服务·架构·负载均衡
CaritoB7 小时前
中台架构下的数据仓库与非结构化数据整合
数据仓库·架构
写bug写bug7 小时前
6 种服务限流的实现方式
java·后端·微服务
韶君7 小时前
Spring Cloud Alibaba-(4)Sentinel【流控和降级】
spring cloud
你知道“铁甲小宝”吗丶8 小时前
【第34章】Spring Cloud之SkyWalking分布式日志
java·spring boot·spring·spring cloud·skywalking
一颗知足的心11 小时前
SpringCloud Alibaba五大组件之——Sentinel
spring·spring cloud·sentinel
一个诺诺前行的后端程序员16 小时前
springcloud微服务实战<1>
spring·spring cloud·微服务