资料参考:《Spring Cloud 微服务实战》
目录
API网关服务:Spring Cloud Zuul
快速入门
构建网关
请求路由
传统路由方式
面向服务的路由
请求过滤
通过前几章的介绍,我们对于Spring Cloud Netflix 下的核心组件已经了解了一大半。这些组件基本涵盖了微服务架构中最为基础的几个核心设施,利用这些组件我们已经可以构建起-一个简单的微服务架构系统,比如,通过使用Spring Cloud Eureka实现高可用的服务注册中心以及实现微服务的注册与发现;通过Spring Cloud Ribbon或Feign实现服务间负载均衡的接口调用:同时,为了使分布式系统更为健壮,对于依赖的服务调用使用SpringCloud Hytrix来进行包装,实现线程隔离并加入熔断机制,以避免在微服务架构中因个别服务出现异常而引起级联故障蔓延。通过上述思路,我们可以设计出类似下图的基础系统架构。
在该架构中,我们的服务集群包含内部服务ServiceA和ServiceB,它们都会向EurekaServer集群进行注册与订阅服务,而Open Service是-一个对外的RESTful API服务,它通过F5、Nginx等网络设备或工具软件实现对各个微服务的路由与负载均衡,并公开给外部的客户端调用。.
在本章中,我们将把视线聚焦在对外服务这块内容,通常也称为边缘服务。首先需要肯定的是,上面的架构实现系统功能是完全没有问题的,但是我们还是可以进一步思考一下,这样的架构是否还有不足的地方会使运维人员或开发人员感到痛苦。
首先,我们从运维人员的角度来看看,他们平时都需要做一些什么工作来支持这样的架构。当客户端应用单击某个功能的时候往往会发出-些对微服务获取资源的请求到后端,这些请求通过F5、Nginx 等设施的路由和负载均衡分配后,被转发到各个不同的服务实例上。而为了让这些设施能够正E确路由与分发请求,运维人员需要手工维护这些路由规则与服务实例列表,当有实例增减或是IP地址变动等情况发生的时候,也需要手工地去同步修改这些信息以保持实例信息与中间件配置内容的一致性。在系统规模不大的时候,维护这些信息的工作还不会太过复杂,但是如果当系统规模不断增大,那么这些看似简单的维护任务会变得越来越难,并且出现配置错误的概率也会逐渐增加。很显然,这样的做法并不可取,所以我们需要一- 套机制来有效降低维护路由规则与服务实例列表的难度。
其次,我们再从开发人员的角度来看看,在这样的架构下,会产生一些怎样的问题呢?大多数情况下,为了保证对外服务的安全性,我们在服务端实现的微服务接口,往往都会有一定的权限校验机制,比如对用户登录状态的校验等:同时为了防止客户端在发起请求时被篡改等安全方面的考虑,还会有一些签名校验的机制存在。这时候,由于使用了微服务架构的理念,我们将原本处于一个应用中的多个模块拆成了多个应用,但是这些应用提供的接口都需要这些校验逻辑,我们不得不在这些应用中都实现这样一套校验逻辑。随着微服务规模的扩大,这些校验逻辑的冗余变得越来越多,突然有一天我们发现这套校验逻辑有个BUG需要修复,或者需要对其做一些扩展和优化,此时我们就不得不去每个应用里修改这些逻辑,而这样的修改不仅会引起开发人员的抱怨,更会加重测试人员的负担。所以,我们也需要一套机制能够很好地解决微服务架构中,对于微服务接口访问时各前置校验的冗余问题。
为了解决上面这些常见的架构问题,API网关的概念应运而生。API 网关是一个更为智能的应用服务器,它的定义类似于面向对象设计模式中的Facade模式,它的存在就像是整个微服务架构系统的门面一样,所有的外部客户端访问都需要经过它来进行调度和过滤。它除了要实现请求路由、负载均衡、校验过滤等功能之外,还需要更多能力,比如与服务治理框架的结合、请求转发时的熔断机制、服务的聚合等一系列高级功能。
既然API网关对于微服务架构这么重要,那么在Spring Cloud中是否有相应的解决方案呢?答案是很肯定的,Spring Cloud中了提供了基于Netfix Zuul实现的API网关组件——Spring Cloud Zuul。那么,它是如何解决上面这两个普遍问题的呢?
首先,对于路由规则与服务实例的维护问题。Spring Cloud Zuul通过与Spring CloudEureka进行整合,将自身注册为Eureka 服务治理下的应用,同时从Eurcka中获得了所有其他微服务的实例信息。这样的设计非常巧妙地将服务治理体系中维护的实例信息利用起来,使得将维护服务实例的工作交给了服务治理框架自动完成,不再需要人工介入。而对于路由规则的维护,Zuul默认会将通过以服务名作为ContextPath的方式来创建路由映射,大部分情况下,这样的默认设置已经可以实现我们大部分的路由需求,除了一些特殊情况(比如兼容一些老的URL)还需要做--些特别的配置。但是相比于之前架构下的运维工作量,通过引入Spring Cloud Zuul实现API网关后,已经能够大大减少了。
其次,对于类似签名校验、登录校验在微服务架构中的冗余问题。理论上来说,这些校验逻辑在本质上与微服务应用自身的业务并没有多大的关系,所以它们完全可以独立成一个单独的服务存在,只是它们被剥离和独立出来之后,并不是给各个微服务调用,而是在API网关服务上进行统一调用来对微服务接口做前置过滤,以实现对微服务接口的拦截和校。Spring Cloud Zuul 提供了一套过滤器机制,它可以很好地支持这样的任务。开发者可以通过使用Zuul来创建各种校验过滤器,然后指定那些规则的请求需要执行校验逻辑,只有通过校验的才会被路由到具体的微服务接口,不然就返回错误提示。通过这样的改造,各个业务层的微服务应用就不再需要非业务性质的校验逻辑了,这使得我们的微服务应用可以更专注于业务逻辑的开发,同时微服务的自动化测试也变得更容易实现。
微服务架构虽然可以将我们的开发单元拆分得更为细致,有效降低了开发难度,但是它所引出的各种问题如果处理不当会成为实施过程中的不稳定因素,甚至掩盖掉原本实施微服务带来的优势。所以,在微服务架构的实施方案中,API网关服务的使用几乎成为了必然的选择。
下面我们将详细介绍Spring Cloud Zuul的使用方法、配置属性以及一些不足之处和需要进行的思考。
介绍了这么多关于API网关服务的概念和作用,在这一节中,我们不妨用实际的示例来直观的体验一下Spring Cloud Zuul中封装的API网关是如何使用和运作,并应用到微服务架构中去的。
首先,在实现各种API网关服务的高级功能之前,我们需要做一些准备工作,比如,构建起最基本的API网关服务,并且搭建几个用于路由和过滤使用的微服务应用等。对于微服务应用,我们可以直接使用之前章节实现的hello-service和feign-consumer.虽然之前我们一直将 feign-consumer视为消费者,但是在Eureka的服务注册与发现体系中,每个服务既是提供者也是消费者,所以feign-consumer实质上也是一个服务提供者。之前我们访问的http://localhost : 9001/feign-consumer等一系列接口就是它提供的服务。读者也可以使用自己实现的微服务应用,因为这部分不是本章的重点,任何微服务应用都可以被用来进行后续的试验。这里,我们详细介绍一下API网关服务的构建过程。
创建一个基础的Spring Boot过程,命名为api-gateway,并在pom.xml中引入spring-cloud-starter-zuul依赖,具体如下:
org.springframework.boot
spring-boot-starter-parent
2.1.1.RELEASE
org.springframework.cloud
spring-cloud-starter-netflix-zuul
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
对于spring-cloud-starter-zuul 依赖,可以通过查看它的依赖内容了解到:该模块中不仅包含了 Netflix Zuul 的核心依赖 zuul-core,它还包含下面这些网关服务的重要依赖。
@SpringBootApplication
@EnableZuulProxy
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
spring.application.name=api-gateway
server.port=5555
完成上面的工作,通过Zuul实现API网关服务就构建完毕了。
下面,我们将通过一个简单的示例来为上面构建的网关服务增加请求路由的功能。为了演示请求路由的功能,我们先将之前准备的Eureka服务注册中心和微服务应用都启动起来。此时,我们在Eureka信息面板中可以看到如下图所示的两个微服务应用已经被注册成功了。
使用Spring Cloud Zuul实现路由功能很简单,只需要对api-gateway服务增加一些关于路由的配置,就你实现传统的路由转发功能,比如:
#传统请求路由
zuul.routes.hello.path=/api-gateway/**
zuul.routes.hello.url=http://localhost:1113/
该配置定义了发往API网关服务的请求中,所有符合/api-gateway/**规则的访问都将被路由转发到http://localhost:1113/ 地址上,也就是说,当我们访问http://localhost :5555/api-gatewayrl/hello的时候,API网关服务会将该请求路由到http://localhost:5555/hello 提供的微服务接口上。其中,配置属性zuul . routes.api-a-url.path中的api-a-url部分为路由的名字,可以任意定义,但是一-组path和url映射关系的路由名要相同,下面将要介绍的面向服务的映射方式也是如此。
很显然,传统路由的配置方式对我们来说并不友好,它同样需要运维人员花费大量时间来维护path和url的关系。为了解决这个问题,Spring Cloud Zuul实现了与Spring Cloud Eureka的无缝整合,我们可以让路由的path不是映射具体的url,而是映射到某个具体的服务,而具体的url交给Spring Cloud Eureka的服务发现机制去自动维护,这就是面向服务的路由。在上面构建网关的示例中使用的就是面向服务的路由。使用这种方式必须注意几点:
org.springframework.cloud
spring-cloud-starter-netflix-eureka-server
2.0.0.RC2
#eureka配置
eureka.client.serviceUrl.defaultZone=http://eureka1:1111/eureka
# 服务路由配置
zuul.routes.api-gateway.path=/api-gateway-service/**
zuul.routes.api-gateway.service-id=PROVIDER-EUREKA
zuul.routes.api-gateway2.path=/api-gateway-service2/**
zuul.routes.api-gateway2.service-id=FEIGN-CONSUMER
针对我们之前准备的两个微服务应用,我们在路由定义中分别配置了两个路由去映射他们。另外,通过eureka,zuul网关服务网也将自己注册到服务注册中心。除了将自己注册成服务,也能获取到服务实例清单,用来实现服务路由的映射。
在完成上面的配置,我们将所有服务启动,可以在注册中心看到:
1:访问http://localhost:5555/api-gateway/hello,服务传统路由规则,符合
zuul.routes.hello.path=/api-gateway/**
zuul.routes.hello.url=http://localhost:1113/
转发到http://localhost:1113/hello
2:访问http://localhost:5555/api-gateway2/index,服务传统路由规则,符合
zuul.routes.hello2.path=/api-gateway2/**
zuul.routes.hello2.url=http://localhost:1115/
转发到http://localhost:1115/index
其他服务路由配置就不一一测试了
在实现了请求路由功能之后,我们的微服务应用提供的接口就可以通过统一的API网关入口被客户端访问到了。但是,每个客户端用户请求微服务的应用提供的接口时,它们的访问权限往往都有一定的限制,系统并不会将所有的微服务接口都对它们开发。然而,目前的服务路由并没有限制权限这样的功能,所有请求都会毫无保留地转发到具体的应用并返回结果,为了实现对客户端请求的安全校验和权限控制,最简单和粗暴的方法就是为每个微服务应用都实现一套用于校验签名和鉴别权限的过滤器或拦截器。不过,这样的做法并不可取,它会增加日后系统的维护难度,因为同一个系统的各种校验逻辑很多情况下都是大致相同或类似的,这样的实现方式使得相似的校验逻辑代码被分散到了各个微服务中去,冗余代码的出现是我们不希望看到的。所以,比较好的做法是将这些校验逻辑剥离出去,构建出一个独立的鉴权服务。在完成剥离之后,有不少开发者会直接再微服务应用中通过调用鉴权服务来实现校验,但是这样的做法仅仅只是解决了权限逻辑的分离,并没有在本质上将这部分不属于冗余的逻辑从原有的微服务应用中拆分出,冗余的拦截器或过滤器依然会存在。
对于这样的问题,更好的做法是通过前置的网关服务来完成这些非业务性质的校验。由于网关服务的假如,外部客户端访问我们的系统已经有了统一入口,既然这些校验与具体业务无关,那何不在请求到达的时候就完成校验和过滤,而不是转发后再过滤而导致更长的请求延迟。同时,通过在网关中完成校验和过滤,微服务应用端就可以去除各种复杂的过滤器和拦截器了,这使得微服务应用接口的开发和测试复杂度得到了相应降低。
为了再API网关中实现对客户端请求的校验,我们将继续介绍Spring Cloud Zuul的另一个核心功能:请求过滤。Zuul允许开发者在API网关上通过定义过滤器来实现对请求的拦截与过滤,实现的方法非常简单,我们之需要继承ZuulFilter抽象类并实现它定义的4个抽象函数就可以完成对请求的拦截和过滤了。
下面的代码定义了一个简单的Zuul过滤器,它实现了在请求被路由之前检查HttpServletRequest中是否有accessToken参数,若有就进行路由,若没有就拒绝访问,返回401 Unauthorized错误。
public class AccessFilter extends ZuulFilter {
private static Logger logger = LoggerFactory.getLogger(AccessFilter.class);
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
/**
* 请求被路由之前检查HttpServletRequest中是否有accessToken参数,若有就进行路由,若没有就拒绝访问,返回401 Unauthorized错误。
* @return
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
logger.info("send {} request to {}", request.getMethod(), request.getRequestURL().toString());
Object accessToken = request.getParameter("accessToken");
if (accessToken == null) {
logger.warn("access token is empty");
context.setSendZuulResponse(false);
context.setResponseStatusCode(401);
return null;
}
logger.info("access token ok");
return null;
}
}
在上面实现都过滤器代码中,我们通过继承ZuulFilter抽象类并重写下面4个方法来实现自定义都过滤器。这4个方法分别定义了如下内容。
在实现了自定义过滤器之后,它并不会直接生效,我们还需要为其创建具体的Bean才能启动该过滤器,比如,在应用启动类中增加如下内容:
@SpringBootApplication
@EnableZuulProxy
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
@Bean
public AccessFilter getAccessFilter(){
return new AccessFilter();
}
}
当然你也可以在AccessFilter使用@Component扫描加载成组件。在对api-gateway-zuul服务完成了上面的改造之后,我们可以重新启动它,并发起下面的请求,对上面定义的过滤器做一个验证。
到这里对于API网关服务的快速入门示例就完成了。通过对Spring Cloud Zuul两个核心功能的介绍,相信读者已经能够体会到API网关服务对微服务架构到重要性了,就目前掌握到API网关知识,我们可以将具体原因总结如下:
实际上,基于Spring Cloud Zuul实现到API网关服务除了上面所示的优点之外,它还有一些更加强大到功能,我们将在后面对其进行更深入的介绍。通过本节的内容,我们只是希望以一个简单到例子带领大家先来简单认识一下API网关服务提供的基础功能以及它在微服务架构中的重要地位。