Spring Cloud系列(二十三) API网关服务Spring Cloud Zuul(Finchley.RC2版本)

为什么使用Spring Cloud Zuul?

通过前几章的介绍,我们对于Spring Cloud Netflix 下的核心组件已经了解了大半,利用这些组件我们已经可以构建一个简单的微服务架构系统,比如通过使用Spring Cloud Eureka实现高可用的服务注册中心以及实现微服务的注册与发现;通过Spring Cloud Ribbon或Feign实现服务间负载均衡的接口调用;同时,为了使分布式系统更健壮,对依赖的服务调用使用Spring Cloud Hystrix来进行包装,实现线程隔离并进入熔断机制,以免在微服务架构中因个别服务出现异常而引起级联故障蔓延。通过上述思路,我们可以设计出类似下图的基础系统架构。

在该架构在,我们的服务集群包含内部服务ServiceA和ServiceB,它们都会向Eureka Server集群进行注册与订阅服务,而Open Service是一个对外的RESTFul API服务,它通过F5、Nginx等网络设备或工具软件实现对各个微服务的路由与负载均衡,并公开给外部的客户端调用。

在接下来的几篇博客内,我们把视线聚焦在对外服务这块内容,通常也称为边缘服务。首先需要肯定的是上面的架构实现系统功能是完全没有问题的,但是这样的架构也有不足之处会使运维人员或开发人员感到痛苦。

首先来说运维人员,他们需要保证客户端发起的请求通过F5或Nginx等设施的路由和负载均衡分配后,把请求分发到各个服务实例,每当有实例增减或IP地址变动时,都需要手动维护这些信息以保持实例信息与中间件配置内容的一致性。如果系统太大,维护起来会变得困难。

然后再从开发人员的角度看,大多数情况下,为了保证对外服务的安全性,我们在服务端实现的微服务接口往往会有一定的权限校验机制,比如对用户登录状态的校验等;同时为了防止客户端在发起请求时被篡改等安全方面的考虑,还会有一些签名校验的机制存在。这时候,由于使用了微服务架构的理念,我们将原本处于一个应用中的多个模块拆成了多个应用,这些应用提供的接口都需要这些校验逻辑,我们不得不在每个应用实现这样一套校验逻辑,随着微服务规模的扩大,这些校验逻辑变得越来越冗余,如果有一天需要改的话,心态肯定爆炸了。

为了解决上面这些问题,API网关的概念应运而生。API网关是一个更为智能的应用服务器,它的定义类似于面向对象设计模式中的Facade模式,它的存在就像是整个微服务架构系统的门面一样,所有的外部客户端访问都需要经过它来进行调度和过滤。它除了要实现请求路由、负载均衡、校验过滤等功能外,还需要更多的能力,比如与服务治理框架的结合、请求转发时的熔断机制、服务的聚合等一系列高级功能。

Spring Cloud Zuul就是基于Netflix Zuul实现的API网关组件。那么它是如何解决上面两个问题的呢?

首先,对于路由规则与服务实例的维护问题。Spring Cloud Zuul和Spring Cloud Eureka进行整合,将自身注册为Eureka服务治理下的应用,同时从Eureka中获得了所有微服务的实例信息。这就使得维护实例的工作交给服务治理框架来完成,不再需要人工介入。对于路由规则的维护,Zuul默认会将通过以服务名作为ContextPath的方式来创建路由映射,大部分情况下这么的默认设置已经可以实现我们大部分的路由需求,除了一些特殊情况(比如兼容老的URL)还需要做一些特别的配置,但是已经大大减少了运维的工作量。

其次,对于类似签名校验、登录校验在微服务架构中的冗余问题。理论上来说这些逻辑在本质上与微服务应用自身的业务没有多大的关系,所以完全可以独立成一个单独的服务存在,只是它们被独立出来后,并不是给各个微服务调用,而是在API网关服务上进行统一调用来对微服务接口做前置过滤,以实现对微服务接口的拦截和校验。Spring Cloud Zuul提供了一套过滤器机制,它可以很好的支持这样的任务。开发者可以通过使用Zuul来创建各种校验过滤器,然后指定哪些规则的请求需要执行哪些校验逻辑,只有通过校验的才会被路由到具体的服务接口,不然就返回错误提示。通过这样的改造,各个业务层的微服务应用就不再需要非业务性质的校验逻辑了,这使得我们的微服务更加专注于业务逻辑的开发。

所以在一个微服务系统架构中,API网关服务的使用几乎成为了必然的选择。

构建网关

首先。在实现各种API网关服务的高级功能之前,我们需要做一些准备各种,必然,构建最基本的API网关服务,并且搭建几个用于路由和过滤使用的微服务应用等。对于微服务应用,我们可以直接使用之前实现的hello-service服务和feign-consumer服务,不清楚的可以看一下我的前几篇博客。虽然之前一直把feign-consumer服务当作消费者,其实在Eureka的服务注册与发现体系中,每个服务既是服务提供者也是服务消费者,所以这里也可以把feign-consumer服务当作服务提供者,它提供的接口就可以当作它提供的服务。现在我来构建一个API网关服务。

第一步,创建一个基本的Spring Boot工程,命名为api-gateway-vFinchley.RC2。

第二步,修改pom.xml,添加对Zuul的依赖

<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>2.0.2.RELEASE</version>
	<relativePath /> <!-- lookup parent from repository -->
</parent>

<properties>
	<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
	<java.version>1.8</java.version>
	<spring-cloud.version>Finchley.RC2</spring-cloud.version>
</properties>

<dependencies>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
	</dependency>

	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-tomcat</artifactId>
		<scope>provided</scope>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-test</artifactId>
		<scope>test</scope>
	</dependency>
</dependencies>

查看Zuul的依赖发现它除了包括Netflix Zuul的核心依赖zuul-core以外,还包含这些网关功能需要的重要依赖:

  1. spring-cloud-starter-netflix-hystrix:该依赖用来在网关服务中实现对微服务转发时候的保护机制,通过线程隔离和断路器,放在微服务的故障引起API网关资源无法释放,从而影响其他应用的对外服务。
  2. spring-cloud-starter-netflix-ribbon:该依赖用于实现在网关服务进行路由转发时候的客户端负载均衡以及请求重试。

第三步,修改应用主类,添加@EnableZuulProxy注解,开启Zuul的API网关服务功能。

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

第四步,修改application.yml文件

spring:
  application:
    name: api-gateway #为服务命名
server:
  port: 5111
eureka:
  client:
    service-url: 
      defaultZone: http://localhost:1111/eureka/ #指定服务注册中心位置
  instance:
    prefer-ip-address: true
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
zuul:
  routes:
    api-a:
      path: /api-a/**
      serviceId: hello-service
    api-b:
      path: /api-b/**
      serviceId: feign-consumer

首先指定服务注册中心的地址为http://localhost:1111/eureka/,服务的端口为5111,服务名为api-gateway;以/api-a/ 开头的请求都转发给hello-service服务;以/api-b/开头的请求都转发给feign-consumer服务。

测试

启动服务注册中心,即eureka-server-vFinchley.Rc2工程

启动服务提供者hello-service,即eureka-client-vFinchley.Rc2 工程

启动服务消费者feign-consumer,即feign-consumer-vFinchley.RC2工程

启动API网关服务api-gateway,即api-gateway-vFinchley.RC2工程。

请求http://localhost:5111/api-a/hello

请求http://localhost:5111/api-b/feign-hello

这就简单的构建了一个API网关服务,而且实现了请求路由的功能。

注意:如果你请求后报错com.netflix.zuul.exception.ZuulException: Forwarding error

查看你的依赖里是否有spring-cloud-starter-netflix-eureka-client,没有的话会报这个错误,至于为什么报这个错,在下面的请求路由里会解释。

请求路由

传统路由方式

使用Spring Cloud Zuul实现路由功能非常简单,只需对api-gateway服务增加一些关于路由规则的配置,就能实现传统的路由转发功能。比如:

zuul.routes.api-a.path= /api-a/**
zuul.routes.api-a.url= http://localhost:2222/

该配置定义了发往API网关的请求中,所有符合/api-a/**规则的访问都将被路由转发到http://localhost:2222/地址上,也就是说当我们请求http://localhost:5111/api-b/hello时,API网关服务会将该请求转发到http://localhost:2222/hello提供的微服务接口上。其中配置属性zuul.routes.api-a.path中的api-a部分为路由的名字,可以任意定义,但是一组path和url映射的路由名必须相同。

修改上面示例中的配置,如下:

zuul:
  routes:
    api-a:
      path: /api-a/**
      url: http://localhost:2222/
    api-b:
      path: /api-b/**
      url: http://localhost:5001/ 

面向服务的路由

很显然,传统路由的配置方式对我们来说并不友好,它同样需要运维人员花费大量时间来维护path和url的关系。为了解决这个问题,Spring Cloud Zuul实现了与Spring Cloud Eureka的无缝整合,我们可以让路由的path不是映射具体的url,而是映射到某个具体的服务,而具体的url交给Spring Cloud Eureka的服务发现机制去自动维护,这就是面向服务的路由。在上面构建网关的示例中使用的就是面向服务的路由。使用这种方式必须注意几点:

  1. 引入了Spring Cloud Eureka依赖,上面示例中说到报错com.netflix.zuul.exception.ZuulException: Forwarding error就是因为没有引入依赖,导致无法注册到服务注册中心。
  2. 指定了服务注册中心

配置方式我就不写了,上面有。

通过面向服务的方式,我们不再需要再为各个路由维护微服务应用的具体实例的位置,而是简单的通过path和serviceId的映射,使维护工作变得简单,建议使用这种方式。

请求过滤

在实现了请求路由功能之后,我们的微服务应用提供的接口就可以通过统一的API网关入口被客户端访问到了。但是微服务应用提供的接口都具有访问权限的限制,然而目前的路由没有限制权限这样的功能,为了实现对客户端请求的安全校验和权限控制,最简单和粗暴的方法就是为每个微服务应用都实现一套用于校验签名和鉴别权限的过滤器和拦截器。不过,这种做法不可取,因为同一个系统的校验逻辑大致是相似的,这样的实现方式会使系统中出现冗余代码,对于日后的维护和扩展增加了难度。比较好的做法就是将这些校验逻辑剥离出去,构建一个独立的校验服务。在完成了剥离后,可以通过调用校验服务来实现校验,但是这仅仅实现了校验逻辑的剥离,还是有大量的过滤器和拦截器存在,没有解决代码冗余的问题。

更好的做法是通过前置的网关服务来完成这些非业务性质的校验。API网关服务是所有请求的统一入口,所以在请求到达时就完成校验和过滤,而不是转发后再过滤而导致更长的请求延迟。同时,通过在网关中完成校验和过滤,微服务应用就可以去除各种复杂的过滤器和拦截器了,降低了开发和维护的难度。

实现请求过滤

第一步,只需要继承ZuulFilter抽象类并实现它定义的四个抽象方法就可以完成对请求的拦截和过滤。

下面是一个对请求中是否有accessToken参数的校验,有的话就路由,没有返回401 Unauthorized错误。

public class AccessFilter extends ZuulFilter{
	private static Logger log = LoggerFactory.getLogger(AccessFilter.class);

	public boolean shouldFilter() {
		// TODO Auto-generated method stub
		return true;
	}
	
	@Override
	public String filterType() {
		// TODO Auto-generated method stub
		return "pre";
	}

	@Override
	public int filterOrder() {
		// TODO Auto-generated method stub
		return 0;
	}


	public Object run() throws ZuulException {
		// TODO Auto-generated method stub
		RequestContext ctx = RequestContext.getCurrentContext();
		HttpServletRequest request = ctx.getRequest();
		
		log.info("send {} request to {}", request.getMethod(), request.getRequestURL().toString());

        Object accessToken = request.getParameter("accessToken");
        if(accessToken == null) {
            log.warn("access token is empty");
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(401);
            return null;
        }
        log.info("access token ok");
        
		return null;
	}

}

上述代码中我们通过继承ZuulFilter抽象类并重写下面四个方法来实现自定义的过滤器。这四个方法分别定义了如下内容:

  1. filterType:过滤器的类型,它决定过滤器在请求的哪个生命周期中执行。这里定义为pre,表示在请求被路由之前执行。
  2. filterOrder:过滤器的顺序。当请求在一个阶段中存在多个过滤器时,需要根据该方法返回的值来依次执行。
  3. shouldFilter:判断该过滤器是否需要被执行。这里直接返回了true,表示对所有请求都生效。实际运用中我们可以通过该方法来指定过滤器的有效范围。
  4. run:过滤器的具体逻辑。这里通过ctx.setSendZuulResponse(false)令zuul过滤该请求,不对其进行路由,然后通过ctx.setResponseStatusCode(401)设置返回的错误码,当然也可以通过ctx.setResponseBody("没有权限")来进一步优化返回的的内容。

第二步,在应用主类创建具体的Bean,启动该过滤器。

@SpringBootApplication
@EnableDiscoveryClient
@EnableZuulProxy
public class Application {
	
	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
	
	@Bean
	public AccessFilter accessFilter() {
		return new AccessFilter();
	}
}

此步骤,也可以改为在AccessFilter 类上加@Component注解。

测试

启动服务注册中心,即eureka-server-vFinchley.Rc2工程

启动服务提供者hello-service,即eureka-client-vFinchley.Rc2 工程

启动API网关服务api-gateway,即api-gateway-vFinchley.RC2工程。

请求http://localhost:5111/api-a/hello返回401错误码

控制台打印日志:

2018-09-17 17:19:41.636  INFO 13148 --- [nio-5111-exec-8] com.wya.springboot.filter.AccessFilter   : send GET request to http://localhost:5111/api-a/hello
2018-09-17 17:19:41.636  WARN 13148 --- [nio-5111-exec-8] com.wya.springboot.filter.AccessFilter   : access token is empty

请求http://localhost:5111/api-a/hello?accessToken=1231313,成功响应

总结

现在大致了解了API网关服务的用法,总结一下:

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

后面会详细介绍API网关提供的功能。

猜你喜欢

转载自blog.csdn.net/WYA1993/article/details/82715324