Spring Cloud 学习笔记 - No.5 服务网关 Zuul

请先阅读之前的内容:

什么是服务网关

在之前的例子中,我们启动了一个外部服务 eureka-consumer,端口 3001
同时我们也启动了两个内部服务 eureka-client,端口 20012002,该外部服务通过 Ribbon 或 Feign 来在客户端负载均衡地调用内部服务
之前我们都是通过 http://127.0.0.1:3001/consumer 来调用外部服务 eureka-consumer 提供的服务 /consumer

问题来了:
假设我们启动了另外一个外部服务 eureka-consumer,端口 3002。此时外部用户只能通过 http://127.0.0.1:3002/consumer 来访问,但是外部用户可能并不知道 3002 这个端口。

服务网关是微服务架构中一个不可或缺的部分。
通过服务网关统一向外系统提供 REST API 的过程中,除了具备服务路由均衡负载功能之外,它还具备了权限控制等功能
Spring Cloud Netflix 中的 Zuul 就担任了这样的一个角色,为微服务架构提供了前门保护的作用,同时将权限控制这些较重的非业务逻辑内容迁移到服务路由层面,使得服务集群主体能够具备更高的可复用性和可测试性。

构建服务网关 api-gateway

可以通过如下的 Spring Assistant 插件来创建项目 api-gateway,添加 Zuul 等作为依赖。

api-gateway 的创建

api-gateway 的创建

api-gateway 的创建

pom.xml 中自动导入了如下的依赖:

<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>

注意,如果是 Finchley 版本的 Spring Cloud,需要再添加如下依赖:

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.7.1</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.7.1</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
</dependency>

否则,启动时会报如下的错误:

ERROR] Failed to execute goal org.springframework.boot:spring-boot-maven-plugin:2.0.3.RELEASE:run (default-cli) on project eureka-consumer: An exception occurred while running. null: InvocationTargetException: Error creating bean with name 'hystrixCommandAspect' defined in class path resource [org/springframework/cloud/netflix/hystrix/HystrixCircuitBreakerConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCommandAspect]: Factory method 'hystrixCommandAspect' threw exception; nested exception is java.lang.NoClassDefFoundError: org/aspectj/lang/JoinPoint: org.aspectj.lang.JoinPoint -> [Help 1]

在主程序中通过 @EnableZuulProxy 注解开启 Zuul 的功能:

@SpringBootApplication
@EnableZuulProxy
public class ApiGatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(ApiGatewayApplication.class, args);
    }
}

application.properties,配置服务名,端口及 Eureka 服务注册中心的地址:

spring.application.name=api-gateway
server.port=7001

eureka.client.serviceUrl.defaultZone=http://localhost:1234/eureka/

最后通过 mvn spring-boot:run 启动该项目,它自己也作为一个服务注册到 Eureka 服务注册中心。它除了会将自己注册到 Eureka 服务注册中心上之外,也会从注册中心获取所有服务以及它们的实例清单。
因此服务网关 Zuul 本身就已经维护了系统中所有 serviceId 与实例地址的映射关系,例如,它知道 eureka-consumer 这个 serviceId 对应到两个地址:

当有外部请求到达服务网关 Zuul 的时候,根据请求的 URL 路径找到最佳匹配的 path 规则,将该请求路由到哪个具体的serviceId 上去,并且通过 Ribbon 来实现负载均衡策略。

http://127.0.0.1:1234/ Eureka 服务注册中心

一个默认的服务网关就构建完毕了。由于 Spring Cloud Zuul 在整合了 Eureka 之后,具备默认的服务路由功能,即:当我们这里构建的 api-gateway 应用启动并注册到 Eureka 之后,服务网关 Zull 会发现上面我们启动的两个服务 eureka-clienteureka-consumer,这时候 Zuul 就会创建路由规则。
每个路由规则都包含两部分,一部分是外部请求的匹配规则,另一部分是路由的服务 ID。针对当前示例的情况,Zuul 会创建下面的四个路由规则,其中:

  • 转发到 eureka-client 服务的请求规则为:/eureka-client/**
  • 转发到 eureka-consumer 服务的请求规则为:/eureka-consumer/**
Zuul 创建的路由规则

在之前的示例中,我们都是通过 http://127.0.0.1:3001/consumer 或者 http://127.0.0.1:3002/consumer 来调用 eureka-consumer 提供的服务 /consumer
在启动了服务网关后,我们就可以通过 http://127.0.0.1:7001/eureka-consumer/consumer 来实现同样的效果,该请求将最终被路由到 eureka-consumer/consumer 接口上。

我们就可以通过 http://127.0.0.1:7001/eureka-consumer/consumer 来实现同样的效果

传统路由配置

所谓的传统路由配置方式就是在不依赖于服务发现机制的情况下,通过在配置文件中具体指定每个路由表达式与服务实例的映射关系来实现 API 网关对外部请求的路由。

没有 Eureka 服务治理框架帮助的时候,我们需要根据服务实例的数量采用不同方式的配置来实现路由规则。
单实例配置:

zuul.routes.eureka-consumer.path=/eureka-consumer/**
zuul.routes.eureka-consumer.url=http://127.0.0.1:3001/

多实例配置:由于存在多个实例,API 网关在进行路由转发时需要实现负载均衡策略,于是这里还需要 Spring Cloud Ribbon 的配合。由于在 Spring Cloud Zuul 中自带了对 Ribbon 的依赖,所以我们只需要做一些配置即可。

zuul.routes.eureka-consumer.path=/eureka-consumer/**
zuul.routes.eureka-consumer.serviceId=eureka-consumer

ribbon.eureka.enabled=false
eureka-consumer.ribbon.listOfServers=http://127.0.0.1:3001/, http://127.0.0.1:3002/

不论是单实例还是多实例的配置方式,我们都需要为每一对映射关系指定一个名称,也就是上面配置中的 <route>,每一个 <route> 就对应了一条路由规则。
每条路由规则都需要通过 path 属性来定义一个用来匹配客户端请求的路径表达式,并通过 urlserviceId 属性来指定请求表达式映射具体实例地址或服务名。

服务路由配置

Spring Cloud Zuul 通过与 Spring Cloud Eureka 的整合,实现了对服务实例的自动化维护,所以在使用服务路由配置的时候,我们不需要向传统路由配置方式那样为 serviceId 去指定具体的服务实例地址,只需要通过一组 zuul.routes.<route>.pathzuul.routes.<route>.serviceId 参数对的方式配置即可,例如:

zuul.routes.eureka-consumer.path=/eureka-consumer/**
zuul.routes.eureka-consumer.serviceId=eureka-consumer

对于面向服务的路由配置,除了使用 pathserviceId 映射的配置方式之外,还有一种更简洁的配置方式:zuul.routes.<serviceId>=<path>,其中 <serviceId> 用来指定路由的具体服务名,<path>用来配置匹配的请求表达式,例如:

zuul.routes.eureka-consumer=/eureka-consumer/**

过滤器

思考这么一个问题:每个客户端用户请求微服务应用提供的接口时,它们的访问权限往往都需要有一定的限制,系统并不会将所有的微服务接口都对它们开放。为了实现对客户端请求的安全校验和权限控制,最简单和粗暴的方法就是为每个微服务应用都实现一套用于校验签名和鉴别权限的过滤器或拦截器。不过,这样的做法并不可取,它会增加日后的系统维护难度,因为同一个系统中的各种校验逻辑很多情况下都是大致相同或类似的,这样的实现方式会使得相似的校验逻辑代码被分散到了各个微服务中去,冗余代码的出现是我们不希望看到的。

对于这样的问题,更好的做法是通过前置的网关服务来完成这些非业务性质的校验。由于网关服务的加入,外部客户端访问我们的系统已经有了统一入口,既然这些校验与具体业务无关,那何不在请求到达的时候就完成校验和过滤,而不是转发后再过滤而导致更长的请求延迟。同时,通过在网关中完成校验和过滤,微服务应用端就可以去除各种复杂的过滤器和拦截器了,这使得微服务应用的接口开发和测试复杂度也得到了相应的降低。

Zuul 允许开发者在 API 网关上通过定义过滤器来实现对请求的拦截与过滤,实现的方法非常简单,我们只需要继承 ZuulFilter 抽象类并实现它定义的四个抽象函数就可以完成对请求的拦截和过滤了:

  • 过滤类型 String filterType(); 在 Zuul 中默认定义了四种不同生命周期的过滤器类型,具体如下:
    • pre:可以在请求被路由之前调用。
    • routing:在路由请求时候被调用。
    • post:在routing和error过滤器之后被调用。
    • error:处理请求时发生错误时被调用。
  • 执行顺序 int filterOrder(); 通过 int 值来定义过滤器的执行顺序,数值越小优先级越高。
  • 执行条件 boolean shouldFilter(); 返回一个 boolean 类型来判断该过滤器是否要执行。我们可以通过此方法来指定过滤器的有效范围。
  • 具体操作 Object run(); 过滤器的具体逻辑。在该函数中,我们可以实现自定义的过滤逻辑,来确定是否要拦截当前的请求,不对其进行后续的路由,或是在请求路由返回结果之后,对处理结果做一些加工等。

路由功能在真正运行时,它的路由映射和请求转发都是由几个不同的过滤器完成的:

  • 路由映射主要通过 pre 类型的过滤器完成,它将请求路径与配置的路由规则进行匹配,以找到需要转发的目标地址;
  • 请求转发route 类型的过滤器来完成,对 pre 类型过滤器获得的路由地址进行转发。

所以,过滤器可以说是 Zuul 实现服务网关功能最为核心的部件,每一个进入 Zuul 的 HTTP 请求都会经过一系列的过滤器处理链得到请求响应并返回给客户端。

图片引自:http://blog.didispace.com/spring-cloud-source-zuul/

请求生命周期

在服务网关 api-gateway 中添加过滤器

我们在上面的项目 api-gateway 中创建 AccessFilter.java

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletRequest;

public class AccessFilter extends ZuulFilter {

    private static Logger log = LoggerFactory.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() {
        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);
            ctx.setResponseBody("unauthorized");
            return null;
        }

        log.info("access token ok");
        return null;
    }
}

随后在主程序中创建具体的 Bean:

@Bean
public AccessFilter accessFilter() {
    return new AccessFilter();
}

重启 api-gateway,访问 http://127.0.0.1:7001/eureka-consumer/consumer

401 未授权错误

401 未授权错误

加上 accessToken 参数访问 http://127.0.0.1:7001/eureka-consumer/consumer?accessToken=12345

正常访问

核心过滤器

核心过滤器

图片引用自:http://blog.didispace.com/spring-cloud-zuul-exception-3/

Spring Cloud Zuul 自带的核心过滤器

拓展阅读

引用自:


引用:
程序猿DD Spring Cloud基础教程
Spring Cloud构建微服务架构:服务网关(基础)【Dalston版】
Spring Cloud构建微服务架构:服务网关(路由配置)【Dalston版】
Spring Cloud构建微服务架构:服务网关(过滤器)【Dalston版】
Spring Cloud Dalston中文文档