zuul学习一:spring cloud zuul的快速入门

前言
通过spring cloud Eureka实现高可用的服务注册中心以及实现微服务的注册与发现;通过spring cloud ribbon或feign实现服务间负载均衡的接口调用,同时,为了分布式系统更为健壮,以避免服务调用使用spring cloud hystrix来进行包装,实现线程隔离并加入熔断机制,以避免在微服务架构中因个别服务出现异常而引起级联故障蔓延。

上面的组件使得我们可以在spring cloud内部之间进行通信,那么怎样对外提供服务呢?

从运维的角度来看看,当客户端应用单击某个功能的时候往往会发出一些对微服务获取资源的请求到后端,这些请求通过F5,Nginx等设施的路由和负载均衡分配后,被转发到各个不同的服务实例上。而为了让这些设施能够正确路由与分发请求,运维人员需要手工维护这些路由规则与服务实例列表,当有实例增减或是IP地址变动等情况发生的时候,也需要手工地同步修改这些信息以保持实例信息与中间件配置内容的一致性。如果当系统规模不断增大,那么这些看似简单的维护任务就变得越来越难,并且出现配置错误的概率也会逐渐增加。很显然,这样的做法并不可取,所以我们需要一套机制来有效降低维护路由规则与服务实例列表的难度。

再从开发者角度来看,大多数的情况下,为了保证对外服务的安全性,我们在服务端实现的微服务接口,往往都会有一定的权限校验机制,比如对用户登录状态的校验等;同时为了防止客户端在发起的请求时被篡改等安全方面的考虑,还会增加一些签名校验的机制存在。在微服务架构中需要对每个模块都去修改,为了简便也需要一套机制能够很好的解决微服务架构中对于微服务接口访问时各前置校验的冗余问题。

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

spring cloud zuul应运而生。

首先,对于路由规则与服务实例的维护问题。spring cloud zuul通过与Eureka进行整合,将自身的服务注册到Eureka上,同时从Eureka中获取所有其他微服务的实例信息。这样的设计,zuul默认将会通过服务名作为ContextPath的方式来创建路由映射,不部分情况下,这样的默认设置已经可以实现大多数的路由需求。除了一些特殊的(比如兼容一些老的url)还需要做一些特别的配置。但是相比与之前的架构下的运维工作量,通过引入spring cloud zuul实现api网关后,已经能够大大减少了。

其次,对于冗余的签名校验,登录校验在微服务架构中的冗余问题。可以独立成一个单独的服务存在,只是他们被剥离和独立出来之后,并不是给各个微服务调用,而是在api网关服务商进行统一调用来对微服务接口做前置过滤,以实现微服务接口的拦截和校验。spring cloud zuul提供了一套过滤机制,它可以很好的支持过滤的任务。

快速入门

定义user,order,pay服务,定义zull服务网关服务都注册到eureka服务上,

通过一下接口访问user,order,pay的服务,

http://localhost:7070/pay/index
http://localhost:8080/user/index
http://localhost:9090/order/index

定义服务网关服务zuul,我们看看其相关配置,zuul-service加入依赖:

<dependencies>
     <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-zuul</artifactId>
     </dependency>
     <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-eureka</artifactId>
      </dependency>
</dependencies>

配置文件:

spring:
  application:
    name: zuul-service
eureka:
  client:
    service-url:
     defaultZone: http://localhost:8761/eureka
  instance:
    instance-id:  ${spring.application.name}:${spring.cloud.client.ipAddress}:${spring.application.instance_id:${server.port}}
    prefer-ip-address: true
server:
  port: 6069

定义启动类:

package com.zhihao.miao;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

//使用@EnableZuulProxy注解开启zuul的api网关服务功能
@SpringBootApplication
@EnableZuulProxy
public class ZuulApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulApplication.class,args);
    }
}

然后可以通过服务网关进行访问上面的三个服务接口,

http://localhost:6069/pay-service/pay/index
http://localhost:6069/user-service/user/index
http://localhost:6069/order-service/order/index

注意
默认的zuul结合eureka会将注册到eureka的服务名作为访问的ContextPath。

  • 也可以指定一些路由机制,application.yml中定义如下,
zuul:
  routes:
    user-service:
      path: /users/**
      serviceId: user-service

url符合/users/**规则的就被转发到user-service实例中了。

http://localhost:6069/users/user/index

之前的通过服务实例名称的也能访问

http://localhost:6069/user-service/user/index
  • 也可以忽略所有的代理,只保留自己配置的即可。
zuul:
  ignored-services: '*'
  routes:
    user-service: /users/**

此时只能访问user-service服务,并且只能以users这种路由来访问。
不能访问

http://localhost:6069/pay-service/pay/index
http://localhost:6069/order-service/order/index

之前默认的注册到eureka上的服务(user-service)也不能访问。
不能访问

http://192.168.1.57:6069/user-service/user/index

可以访问

http://192.168.1.57:6069/users/user/index

以上的配置也可以这样指定

zuul:
  ignored-services: order-service,pay-service
  routes:
    user-service: /users/**

当然此时除了http://192.168.1.57:6069/users/user/index能被代理访问到
http://192.168.1.57:6069/user-service/user/index也能访问了

请求过滤

在实现请求理由功能之后,我们的微服务应用提供的接口就可以通过统一的api网关入口被客户端访问到了。每个客户端用户请求服务应用提供的接口时,他们的访问权限往往都有一定的限制,系统并不会将所有的微服务接口对他们开放。然而,目前的服务路由并没有限制权限这样的功能,所有请求都会被毫无保留的转发到具体的应用并返回结果,为了实现对客户端请求的安全校验和权限控制,最简单和粗暴的方法就是在每个微服务应用都实现一套用于校验签名和鉴别权限的过滤器或拦截器。这样有个问题就是功能实现太过冗余。比较好的做法就是将这些校验逻辑剥离出去,构建一个独立的鉴权服务。在完成剥离之后,直接在微服务应用中通过调用鉴权系统服务来实现校验,但是这样仅仅只是解决了鉴权逻辑的分离,并没有在本质上将这部分不属于冗余的逻辑从原有的微服务应用中拆出去,冗余的拦截器或者过滤器依然会存在。

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

在zuul-service服务中定义一个过滤器,

public class AccessFilter extends ZuulFilter{

    private Logger logger = LoggerFactory.getLogger(getClass());

    //过滤器的类型,它决定过滤器在请求的哪个生命周期中执行,这里定义为pre,代表会在请求被理由之前执行。
    @Override
    public String filterType() {
        return "pre";
    }

    //过滤器的执行顺序。当请求在一个阶段中存在多个过滤器时,需要根据该方法返回的值来依次执行
    @Override
    public int filterOrder() {
        return 0;
    }

    //判断该过滤器是否需要被执行。这里我们直接返回了true,因此该过滤器对所有的请求都生效。实际运行中我们可以利用该函数
    //来指定过滤器的有效范围。
    @Override
    public boolean shouldFilter() {
        return true;
    }

    //过滤器的具体执行逻辑。
    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();

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

        Object accessToken = request.getParameter("accessToken");
        if(accessToken == null){
            logger.warn("access token is empty");
            ctx.setSendZuulResponse(false); //令zuul过滤该请求,不对其进行路由
            ctx.setResponseStatusCode(401); //设置返回的错误码
        }

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

ZuulFilter接口中定义四个方法:
filterType:过滤器的类型,它决定过滤器在请求的哪个生命周期中执行,这里定义为pre,代表会在请求被理由之前执行。
filterOrder:过滤器的执行顺序。当请求在一个阶段中存在多个过滤器时,需要根据该方法返回的值来依次执行。
shouldFilter:判断该过滤器是否需要被执行。这里我们直接返回了true,因此该过滤器对所有的请求都生效。实际运行中我们可以利用该函数。
run:过滤器的具体执行逻辑。

在将过滤器纳入spring容器中,

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

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

访问服务:

http://localhost:6069/users/user/index

状态码错误为401,正确访问姿势是

http://localhost:6069/users/user/index?accessToken=111

过滤器类型与请求生命周期

  • PRE:这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
  • ROUTING:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用Apache HttpClient或Netfilx Ribbon请求微服务。
  • POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
  • ERROR:在其他阶段发生错误时执行该过滤器。

除了默认的过滤器类型,Zuul还允许我们创建自定义的过滤器类型。例如,我们可以定制一种STATIC类型的过滤器,直接在Zuul中生成响应,而不将请求转发到后端的微服务。

总结

就目前掌握的api网关知识,总结出四点如下

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

参考博客
Spring Cloud Zuul过滤器详解
使用 API 网关构建微服务
spring cloud zuul官网地址

本博客代码
代码地址

推荐阅读更多精彩内容