API 网关服务:Spring Cloud Zuul(一):背景和快速入门程序

《Spring Cloud 微服务实战》 - 瞿永超 著

缺少一些之前的关于Ribbon、Hystrix、Feign 的文章,有机会来补全

为什么需要API 网关?

  利用之前的几个组件,我们已经可以搭建起一个简单的微服务架构系统,比如,通过使用Spring Cloud Eureka 实现高可用的服务注册中心以及实现微服务的注册与发现;通过 Spring Cloud Ribbon 或 Feign 实现服务间负载均衡的接口调用;同时,为了使分布式系统更为健壮,对于依赖的服务调用使用 Spring Cloud Hystrix 进行包装,实现线程隔离并加入熔断机制,以避免在微服务架构中因个别服务出现异常而引起级联故障蔓延。类型下图的基础系统架构。
基础的微服务架构
  在这里,我们看到对外服务这块[Open Service],通常也称为边缘服务。首先肯定的是,上面的架构实现系统功能完全没有问题,但我们进一步的思考,这样的架构是否还有不足的地方会使运维人员或开发人员感到痛苦。
  从运维角度来看,当客户端应用单击某个功能的时候往往会发出一些对微服务获取资源的请求到后端,这些请求通过F5、Nginx 等设施的路由和负载均衡分配后,被转发到各个不同的服务实例上。而为了让这些设施能够正确路由与分发请求,运维人员需要手工维护这些路由规则与服务实例列表,当有实例增减或是IP地址变动等情况发生的时候,也需要手工地去同步修改这些信息以保持实例信息与中间件配置内容的一致性。此时,如果系统规模不大,这些工作不会太复杂,但是当系统规模不断增大,那么这些简单的维护任务会变得越来越难,并且出现配置错误的概率也会逐渐增加。因此,我们需要一套机制来有效降低维护路由规则与服务实例列表的难度。
  从开发人员角度来看,大多数情况下,为了保证对外服务的安全性,我们在服务端实现的微服务接口,往往都会有一定的权限校验机制,比如对用户登录状态的校验等;同时为了防止客户端在发起请求时被篡改等安全方面的考虑,还会有一些签名校验的机制存在。但由于使用了微服务的架构理念,我们将原本处于一个应用中的多个模块拆分成了多个应用,但是这些应用提供的接口都需要这些校验逻辑,我们不得不在这些应用中都实现这样一套校验逻辑。随着微服务规模的扩大,这些校验逻辑的冗余变得越来越多,如果有一天发现这套逻辑有个BUG需要修复,或者需要对其做一些扩展和优化,此时我们就不得不去每个应用里修改这些逻辑,这样的修改不仅会引起开发人员的抱怨,更会加重测试人员的负担。因此,我们也需要一套机制能够很好地解决微服务架构中,对于微服务接口访问时各前置校验的冗余问题。
  为了解决上面的问题,API 网关的概念应运而生。API 网关是一个更为智能的应用服务器,它的定义类似于面向对象设计模式中的外观(Facade)模式,它的存在就像是整个微服务架构系统的门面一样,所有的外部客户端访问都需要经过它来进行调度和过滤它除了要实现请求路由、负载均衡、校验过滤等功能之外,还需要更多的,比如与服务治理框架的结合、请求转发时的熔断机制、服务的聚合等高级功能

Spring Cloud Zuul

  在 Spring Cloud 中提供了基于 Netflix Zuul 实现的 API 网关组件-Spring Cloud Zuul。首先,对于路由规则与服务实例的维护问题。Spring Cloud Zuul 通过与 Spring Cloud Eureka 进行整合,将自身注册为Eureka 服务治理下的应用,同时从 Eureka 中获取其他微服务的实例信息。这使得维护服务实例的工作交给了服务治理框架自动完成,不再需要人工介入。而对于路由规则的维护,Zuul 默认会通过已服务名作为ContextPath 的方式来创建路由映射,极少的情况下,可能需要特别的配置。
  对于类似签名校验、登录校验在微服务架构中的冗余问题。理论上来说,这些校验逻辑就本质上与微服务应用自身的业务并没有多大关系,它们可以完全独立成一个单独的服务存在,只是它们被剥离和独立出来之后,并不是给各个微服务调用,而是在API 网关服务上进行统一调用来对微服务接口做前置过滤,以实现对微服务接口的拦截和校验。Zuul 提供了一套过滤器机制,开发者通过使用Zuul来创建各种校验过滤器,然后指定哪些规则的请求需要执行校验逻辑,只有通过校验的才会被路由到具体的微服务接口,不然就返回错误提示。这些下来,各个业务员层的微服务应用就不再需要非业务性质的校验逻辑了,使得微服务应用可以更专注于业务逻辑的开发,同时微服务的自动化测试也变得更容易实现。
  微服务架构虽然可以将我们开发单元拆分得更为细致,有效降低了开发难度,但是它所引出的各种问题如果处理不当会成为实施过程中的不稳定因素,甚至掩盖掉原本实施微服务带来的优势。所以,在微服务架构的实施方案中,API 网关服务的使用几乎成为了必然的选择

快速入门
构建网关
  • 创建一个Spring boot工程,引入spring-cloud-starter-zuul 依赖
      (查看 zuul 的依赖可知,不仅包含了核心依赖 zuul-core, 还包含了下面的网关服务需要的重要依赖)
      spring-cloud-starter-hystrix: 容错保护
      spring-cloud-starter-ribbon: 客户端负载均衡以及请求重试
      spring-cloud-starter-actuator: 常规的微服务管理端点,还添加了 /routers 端点返回当前的所有路由规则

  • 在程序的启动入口上,添加 @EnableZuulProxy 注解开启Zuul 的API 网关服务功能

@EnableZuulProxy
@SpringBootApplication
public class ApiGatewayApplication {
   public static void main(String[] args) {
      SpringApplication.run(ApiGatewayApplication.class, args);
   }
}
请求路由

包含两种方式: 传统路由,面向服务的路由

传统路由

  只需要对api-gateway 服务增加一些关于路由规则的配置,就能实现传统的路由转发功能:

zuul:
  routes:
    # 传统路由配置
    api-a-url:
      path: /api-a-url/**
      url: http://localhost:8080/

  如上的配置定义了发往API网关服务的请求中,所有符合 /api-a-url/** 规则的访问都将被路由转发到 http://localhost:8080/ 地址上。其中,配置属性 api-a-url 部分为路由的名字,可以任意定义,但是一组的path 和 url的映射关系路由名要相同

面向服务的路由

  很显然,传统路由的配置方式对于我们来说并不友好,它统一需要运维人员花费大量时间来维护各个路由path和url的关系。为了解决该问题,Spring Cloud Zuul 实现了与 Spring Cloud Eureka 的无缝整合,我们可以让路由的path不是映射具体的url,而是让它映射到某个具体的服务,而具体的url 则交给 Eureka 的服务发现机制去自动维护,我们称之为面向服务的路由。

  • 为了与Eureka 整合,需要在pom.xml 中添加Eureka 的依赖
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
  • 在配置文件中配置指向服务的路由,同时指导Eureka注册中心的位置
zuul:
  routes:
    # 面向服务的路由配置
    api-a:
      path: /api-a/**
      serviceId: hello-service
    api-b:
      path: /api-b/**
      serviceId: feign-consumer

eureka:
  client:
    service-url:
      defaultZone: http://192.168.6.181:1111/eureka/

  通过指定服务注册中心的位置,除了将自己注册成服务之外,同时也让Zuul 能够获取到Eureka维护的实例清单, 以实现path 映射服务,再从服务中挑选实例来进行请求转发的完整路由机制。
  完成以上配置后,我们启动 Eureka 、 hello-service、 feign-consumer 以及这里的api-gateway 服务。启动完毕后,在Eureka的面板中,我们可以看到这三个服务提供者。
  根据配置关系,我们通过服务网关来访问 hello-service 和 feign-consumer 这两个服务,分别向网关发起下面的请求:

  • http://localhost:5555/api-a/hello : 该url 符合 /api-a/** 规则,由 /api-a/** 负责转发,该路由映射到的serviceId 为 hello-service,所以最终 /hello 请求会被发送到 hello-service 服务的某个实例上去。
  • http://localhost:5555/api-b/feign-consumer2 : 与上面类似,最终 /feign-consumer2 请求 被发送到 feign-consumer 服务的某个实例上去。
    在这里插入图片描述
      通过上面的配置,我们不需要再为各个路由维护微服务应用的具体实例位置,而是通过简单的path 与 serviceI的映射组合,使得维护工作变的非常简单。API 网关服务可以自动化完成服务实例清单的维护。
请求过滤

  每个客户端用户请求微服务应用提供接口时,它们的访问权限往往都有一定的限制,系统并不会将所有的微服务接口都对它们开放。为了实现对客户端请求的安全校验和权限控制,最简单粗暴的方法就是为每个微服务应用都实现一套用于校验签名和鉴别权限的过滤器或拦截器。不过,这样做不可取,会增加日后系统的维护难度。因为同一系统中,各校验逻辑很大程度上都是大致相同或类似的,这样的实现方式会使得相似的逻辑代码被分散到各个微服务中去,形成冗余代码。所以,比较好的做法是将这些校验逻辑剥离出去,构建出一个独立的鉴权服务。在完成剥离之后,有不少开发者会直接在微服务中通过调用鉴权服务来实现校验,但这仅仅只解决了鉴权逻辑的分离,并没有在本质上将这部分不属于冗余的逻辑从原有的微服务应用中拆分出,冗余的拦截器或过滤器依旧存在
  更好的做法是通过前置的网关服务来完成这些非业务性质的校验。由于网关服务的加入,外部客户端访问我们的系统已经有了统一的入口,那为何不在请求到达的时候就完成校验和过滤,而不是转发后在过滤而导致更长的请求延迟。同时,通过在网关中完成校验和过滤,微服务应用端就可以去除各种复杂的过滤器和拦截器,降低了接口的开发和测试复杂度。
  为了在 API 网关中实现对客户端请求的过滤,Zuul 允许开发者在 API 网关上通过定义过滤器来实现对请求的拦截和过滤,我们只需要基础ZuulFilter 抽象类并实现它定义的4个抽象方法就可以完成对请求的拦截和过滤。
  下面的代码展示了简单的过滤器,实现了正在请求路由之前检测 HttpServletRequest 中是否有 accessToken 参数,若有就进行路由,没有这拒绝访问,返回401 Unauthorized 错误。

/**
*
* describe 定义过滤器来实现对请求的拦截和过滤
* @author xmc
* @date 2019/1/24 11:25
* @param  * @param null
* @return
*/
public class AccessFilter extends ZuulFilter {

   private final Logger log = LogManager.getLogger(AccessFilter.class);

   @Override
   public String filterType() {
      return "pre";
   }

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

   @Override
   public boolean shouldFilter() {
      return true;
   }

   @Override
   public Object run() throws ZuulException {
      // 获取 zuul 的上下文
      RequestContext ctx = RequestContext.getCurrentContext();
      HttpServletRequest request = ctx.getRequest();

      log.info("send {} request to {}", request.getMethod(), request.getRequestURI());

      // 校验请求中是否存在accessToken
      String token = request.getParameter("accessToken");
      if (token == null) {
         // 不存在,不允许通过网关,直接返回401
         log.warn("accessToken is empty!");
         ctx.setSendZuulResponse(false);
         ctx.setResponseStatusCode(401);
         return null;
      }
      log.info("access token ok");
      return null;
   }
}

4个方法的定义如下:

  • filterType: 过滤器的类型,它决定过滤器在请求的哪个生命周期中执行。这里的pre,代表在请求被路由之前执行。
  • filterOrder: 过滤器的执行顺序。当请求在一个阶段中存在多个过滤器时,需要根据该方法返回的值来依次执行。
  • shouldFilter: 判断该过滤器是否需要被执行。这里直接返回了true,因此该过滤器对所有请求都会生效,实际运用中可以利用该函数来指定过滤器的有效范围。
  • run: 过滤器的具体逻辑。这里通过 ctx.setSendZuulResponse(false) 令 zuul 过滤该请求,不对其进行路由,然后通过 ctx.setResponseStatusCode(401) 设置其返回的错误码。

  实现了自定义过滤器之后,不会直接生效,需要将其bean交由Spring 管理,即可生效。一种是在启动程序中注入bean,另一种自定义配置,使用@Component 注入到Spring中:

@Component
public class FilterConfig {
   // 注入校验过滤器
   @Bean
   public AccessFilter accessFilter() {
      return new AccessFilter();
   }
}

在对上面的服务完成了改造之后我们重新启动它,发起下面的请求,对定义的过滤器做验证:

目前了解程度,Spring Cloud Zuul 实现的 API 网关服务有如下几个有点:

  • 作为系统的统一入口,屏蔽了系统内部各个微服务的细节。
  • 与服务治理框架结合,实现自动化的服务实例维护以及负载均衡的路由转发。
  • 实现接口权限校验与微服务业务逻辑的解耦。
  • 通过服务网关中的过滤器,在各声明周期中去校验请求的内容,将原本在对外服务层做的校验前移,保证了微服务的无状态性,同时降低了微服务的测试难度,让服务层更关注业务逻辑的处理。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值