一,网关

前面我们把服务治理,服务注册发现,服务调用,熔断,已经分析完了,微服务基本模块已经有了,也可以做微服务了。但完成一个复杂的业务,可能需要多个微服务合作来完成,比如下单,需要用户服务,支付服务,地图服务,订单服务。网关一般是我们对外服务的窗口,进行服务内外隔离。一般微服务都在内网,不做安全验证。网关是介于客户端(外部调用方比如app,h5)和微服务的中间层。

什么是Zuul?

Zuul作为微服务系统的网关组件,是从设备和网站到Netflix流应用程序后端的所有请求的前门。zuul作为整个应用的流量入口,接收所有的请求,如app、网页等,并且将不同的请求转发至不同的处理微服务模块。作为边缘服务应用程序,Zuul旨在实现动态路由,监控,弹性和安全性。

Zuul 主要应用场景

  1. 黑白名单:实现通过IP地址控制禁止访问网关功能,此功能是应用层面控制实现,再往前也可以通过网络传输方面进行控制访问。
  2. 日志:实现访问日志的记录,可用于分析访问、处理性能指标,同时将分析结果支持其他模块功能应用。
  3. 协议适配:实现通信协议校验、适配转换的功能。
  4. 身份认证:负责网关访问身份认证验证,此模块与“访问认证中心”通信,实际认证业务逻辑交移“访问认证中心”处理。
  5. 计流限流:实现微服务访问流量计算,基于流量计算分析进行限流,可以定义多种限流规则。
  6. 路由:路由是API网关很核心的模块功能,此模块实现根据请求,锁定目标微服务并将请求进行转发。此模块需要与“服务发布管理中心”通信。“服务发布管理中心”实现微服务发布注册管理功能,与其通信获得目标微服务信息。

二,Zuul使用

第一步:新建模块zuul-service

引入zuul和eureka client的依赖

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

第二步:在启动类上加@EnableZuulProxy

@SpringBootApplication
@EnableZuulProxy
public class EurekaZuulApplication {

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

}

第三步:添加application.yml配置

spring:
  application:
    name: zuul-service
eureka:
  client:
    service-url:
      defaultZone: http://euk-server1:7001/eureka/
  instance:
    hostname: euk-client2
server:
  port: 7008
zuul:
  routes:
	# 标识你服务的名字,这里可以自己定义,一般方便和规范来讲还是跟自己服务的名字一样
    eureka-provider:
	  # 服务映射的路径,通过这路径就可以从外部访问你的服务了,目的是为了不爆露你机器的IP
      path: /eureka-provider/**
	  # 这里一定要是你Eureka注册中心的服务的名称,是所以这里配置serviceId因为跟eureka结合了
      serviceId: eureka-provider

以上步骤完成,接下来我们依次启动eureka-server,eureka-provider,eureka-zuul模块

访问http://euk-client2:7008/eureka-provider/getOrder输出:

在这里插入图片描述

三,Zuul路由

Zuul的路由包含两种路由:

  1. 传统路由:所谓的传统路由配置方式就是在不依赖于服务发现机制的情况下,通过在配置文件中具体指定每个路由表达式与服务实例的映射关系来实现 API 网关对外不请求的路由。
  2. 面向服务路由:传统路由的配置方式需要运维人员花费大量的时间来维护各个路由 path 与 url 的关系。为了解决这个问题,Spring Cloud Zuul 实现了与 Spring Cloud Eureka 的无缝整合,我们可以让路由的 path 不是映射具体的 url ,而是让它映射到某个具体的服务,而具体的 url 则交给 Eureka 的服务发现机制去自动维护。这类的路由便称为面向服务的路由。

1,传统路由配置

单实例配置

单实例的路由转发通过 zuul.routes..pathzuul.routes..url 参数对的方式进行配置。

比如下面配置实现了对符合 /zuul-service/** 规则的请求路径转发到 http://localhost:8888/ 地址的路由规则

zuul.routes.zuul-service.path=/zuul-service/**
zuul.routes.zuul-service.url=http://localhost:8888/
多实例配置

多实例的路由转发通过 zuul.routes..pathzuul.routes..service-id 参数对的方式进行配置,其中 service-id 是由用户手工命名的服务名称,配合 ribbon.listOfServers 参数实现服务与实例的维护:

比如下面配置实现了对符合 /my-service/** 规则的请求路径转发到 http://localhost:8888/http://localhost:9999/ 两个实例地址的路由规则。

zuul.routes.zuul-service.path=/zuul-service/**
zuul.routes.zuul-service.service-id=zuul-service
zuul-service.ribbon.listOfServers=http://localhost:8888/,http://localhost:9999/

2,面向服务路由配置

默认规则

当我们为 Spring Cloud Zuul 构建的 API 网关服务引入 Spring Cloud Eureka 之后,它会为 Eureka 中的每个服务都自动创建一个默认路由规则:使用服务名作为 path 请求前缀。

比如 http://192.168.0.128:8888/order-service/order这个请求就会直接转发到 ORDER-SERVICE 服务实例上。

关闭默认规则

我们可以通过zuul.ignored-services=order-service 配置需要忽略的微服务(多个微服务通过逗号隔开),这样就不会自动对其创建路由规则。

我们也可以使用zuul.ignored-services=* 对所有的服务都不自动创建路由规则。

3,路径匹配

不论是使用传统路由的配置方式还是服务路由的配置方式,我们都需要为每个路由规则定义匹配表达式,也就是上面所说的path参数。在Zuul中,路由匹配的路径表达式采用了Ant风格定义。

Ant风格的路径表达式使用起来非常简单,它一共有下面这三种通配符:

通配符说明
?匹配任意的单个字符
*****匹配任意数量的字符
**匹配任意数量的字符,支持多级目录

通配符实例演示:

/user-service/?

它可以匹配/user-service/之后拼接一个任务字符的路径,比如:/user-service/a、/user-service/b、/user-service/c

/user-service/*

它可以匹配/user-service/之后拼接任意字符的路径,比如:/user-service/a、/user-service/aaa、/user-service/bbb。但是它无法匹配/user-service/a/b

/user-service/**

它可以匹配/user-service/*包含的内容之外,还可以匹配形如/user-service/a/b的多级目录路径

4,忽略表达式

通过path参数定义的ant表达式已经能够完成api网关上的路由规则配置功能,但是为了更细粒度和更为灵活地配置理由规则,zuul还提供了一个忽略表达式参数zuul.ignored-patterns。

该参数可以用来设置不希望被api网关进行路由的url表达式。

注意:该参数在使用时还需要注意它的范围并不是针对某个路由,而是对所有路由。所以在设置的时候需要全面考虑url规则,防止忽略了不该被忽略的url路径。

比如我们启动order-service服务,访问:http://192.168.1.57:6069/order/index

可以使用api网关路由:http://192.168.1.57:6069/order-service/order/index

在zuul-service中配置:

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
zuul:
ignoredPatterns: /**/index/**
  routes:
    user-service:
      path: /user-service/**
      serviceId: user-service
    order-service:
      path: /order-service/**
      serviceId:  order-service
logging:
  level:
    com.netflix: debug

设置后如果访问 http://localhost:8888/order-service/index 将不会被正确路由,因为该路径符合 zuul.ignored-patterns 参数定义的规则。而其他路径则不会有问题,比如 http://localhost:8888/order-service/getOrderInfo。

5,路由前缀

Zuul通过zuul.prefix参数来为路由规则增加前缀信息:

zuul:
  routes:
    eureka-provider:
      path: /eureka-provider/**
      serviceId: eureka-provider
  prefix: /bobo

配置完前缀之后,之前访问路径都要增加 /bobo前缀:

未加前缀时访问 user-service 服务:http://localhost:8888/eureka-provider/hello

添加前缀后访问 hello-service 服务:http://localhost:8888/bobo/eureka-provider/hello

Zuul通过strip-prefix代理前缀默认会从请求路径中移除,通过该设置关闭移除功能

stripPrefix=true 的时 (会移除)
(http://127.0.0.1:8888/bobo/user/list**->** http://192.168.1.100:8080/user/list)

stripPrefix=false的时(不会移除)

http://127.0.0.1:8888/bobo/user/list **->**http://192.168.1.100:8080/bobo/user/list

6,本地跳转

在 Zuul 实现的 API 网关路由功能中,还支持 forward 形式的服务端跳转配置。

比如我们在 API 网关项目中增加一个 /local/helloWorld 的接口

@RestController
public class HelloController {
    @RequestMapping("/local/helloWorld")
    public String hello() {
        return "hello word!";
    }
}

然后在 增加一个本地跳转的路由规则(forward-local):

zuul.routes.forward-local.path=/forward-local/**
zuul.routes.forward-local.url=forward:/local

当 API 网关接收到请求 /forward-local/helloWorld,它符合 forward-local 的路由规则,所以该请求会被 API 网关转发到网关的 /local/helloWorld请求上进行本地处理。

7,cookie与头信息

Spring Cloud Zuul 在请求路由时,通过zuul.sensitiveHeaders 参数定义,包括Cookie、Set-Cookie、Authorization 三个属性来过滤掉HTTP请求头信息中的一些敏感信息,防止它们被传递到下游的外部服务器。

但是如果我们将使用了Spring Security、Shiro 等安全框架构建的 Web 应用通过 Spring Cloud Zuul 构建的网关来进行路由时,Cookie 信息无法传递,会导致无法实现 登录和鉴权。

如何解决:

通过指定路由的参数来设置,仅对指定的web应用开启敏感信息传递:

# 对指定路由开启自定义敏感头
zuul.routes.<router>.customSensitiveHeaders=true
# 将指定路由的敏感头信息设置为空
zuul.routes.<router>.sensitiveHeaders=[这里设置要过滤的敏感头]

注意:指定路由的敏感头配置会覆盖掉全局设置 ‍‍‍‍‍‍

8,重定向问题

什么是Zuul的重定向问题?

图片

我们在浏览器中通过 Zuul 网关发起了认证服务,认证通过后会进行重定向到某个主页或欢迎页面。此时,我们发现,在认证完成之后,但是发现重定向的这个欢迎页的 host 变成了这个认证服务的 host,而不是 Zuul 的 host,这是一个很严重的问题。

解决方法:

### 网关配置
zuul:
  routes:
    demo-order:
      path: /do/**
      serviceId: demo-order
      stripPrefix: true
      sensitiveHeaders: Cookie,Set-Cookie,Authorization
  # 此处解决后端服务重定向导致用户浏览的 host 变成 后端服务的 host 问题
  add-host-header: true

四,过滤器

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

我们可以在ZuulFilter接口中看到定义的4个抽象方法,这四个抽象方法也就代表了过滤器的四个核心概念:

// 类型Type:定义在路由流程中,过滤器被应用的阶段
String filterType();
// 执行顺序Execution Order:在同一个Type中,定义过滤器执行的顺序
int filterOrder();
// 条件Criteria:过滤器被执行必须满足的条件
boolean shouldFilter();
// 动作Action:过滤器的具体逻辑。在该函数中,我们可以实现自定义的过滤逻辑,来确定是否要拦截当前的请求,
// 不对其进行后续的路由,或是在请求路由返回结果之后,对处理结果做一些加工等。
Object run();

1,类型type

Zuul中定义了四种标准过滤器类型,这些过滤器类型对应于请求的典型生命周期。

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

2,核心过滤器详解

核心过滤器执行顺序:

在这里插入图片描述

前置过滤器
1,ServletDetectionFilter

它的执行顺序为-3,是最先被执行的过滤器。

该过滤器总是会被执行,主要用来检测当前请求是通过Spring的DispatcherServlet处理运行的,还是通过ZuulServlet来处理运行的。

它的检测结果会以布尔类型保存在当前请求上下文的isDispatcherServletRequest参数中,这样后续的过滤器中,我们就可以通过RequestUtils.isDispatcherServletRequest()和RequestUtils.isZuulServletRequest()方法来判断请求处理的源头,以实现后续不同的处理机制。

一般情况下,发送到api网关的外部请求都会被Spring的DispatcherServlet处理,除了通过/zuul/*路径访问的请求会绕过DispatcherServlet(比如之前我们说的大文件上传),被ZuulServlet处理,主要用来应对大文件上传的情况。

另外,对于ZuulServlet的访问路径/zuul/*,我们可以通过zuul.servletPath参数进行修改。

在这里插入图片描述

2,Servlet30WrapperFilter

它的执行顺序为-2,是第二个执行的过滤器,目前的实现会对所有请求生效,主要为了将原始的HttpServletRequest包装成Servlet30RequestWrapper对象。

在这里插入图片描述

3,FormBodyWrapperFilter

它的执行顺序为-1,是第三个执行的过滤器。该过滤器仅对两类请求生效,第一类是Context-Type为application/x-www-form-urlencoded的请求,第二类是Context-Type为multipart/form-data并且是由String的DispatcherServlet处理的请求(用到了ServletDetectionFilter的处理结果)。

而该过滤器的主要目的是将符合要求的请求体包装成FormBodyRequestWrapper对象。

在这里插入图片描述

4,DebugFilter

它的执行顺序为1,是第四个执行的过滤器,该过滤器会根据配置参数zuul.debug.request和请求中的debug参数来决定是否执行过滤器中的操作。

而它的具体操作内容是将当前请求上下文中的debugRoutingdebugRequest参数设置为true。

由于在同一个请求的不同生命周期都可以访问到这二个值,所以我们在后续的各个过滤器中可以利用这二个值来定义一些debug信息,这样当线上环境出现问题的时候,可以通过参数的方式来激活这些debug信息以帮助分析问题,另外,对于请求参数中的debug参数,我们可以通过zuul.debug.parameter来进行自定义。

在这里插入图片描述

5,PreDecorationFilter

执行顺序是5,是pre阶段最后被执行的过滤器,该过滤器会判断当前请求上下文中是否存在forward.doserviceId参数,如果都不存在,那么它就会执行具体过滤器的操作(如果有一个存在的话,说明当前请求已经被处理过了,因为这二个信息就是根据当前请求的路由信息加载进来的)。

而当它的具体操作内容就是为当前请求做一些预处理,比如说,进行路由规则的匹配,在请求上下文中设置该请求的基本信息以及将路由匹配结果等一些设置信息等,这些信息将是后续过滤器进行处理的重要依据,我们可以通过RequestContext.getCurrentContext()来访问这些信息。

另外,我们还可以在该实现中找到对HTTP头请求进行处理的逻辑,其中包含了一些耳熟能详的头域,比如X-Forwarded-Host,X-Forwarded-Port

另外,对于这些头域是通过zuul.addProxyHeaders参数进行控制的,而这个参数默认值是true,所以zuul在请求跳转时默认会为请求增加X-Forwarded-*头域,包括X-Forwarded-Host,X-Forwarded-PortX-Forwarded-ForX-Forwarded-Prefix,X-Forwarded-Proto

也可以通过设置zuul.addProxyHeaders=false关闭对这些头域的添加动作。

路由过滤器
6,RibbonRoutingFilter

它的执行顺序为10,是route阶段的第一个执行的过滤器。

该过滤器只对请求上下文中存在serviceId参数的请求进行处理,即只对通过serviceId配置路由规则的请求生效。

而该过滤器的执行逻辑就是面向服务路由的核心,它通过使用ribbon和hystrix来向服务实例发起请求,并将服务实例的请求结果返回。

在这里插入图片描述

在这里插入图片描述

7,SimpleHostRoutingFilter

它的执行顺序为100,是route阶段的第二个执行的过滤器。

该过滤器只对请求上下文存在routeHost参数的请求进行处理,即只对通过url配置路由规则的请求生效。

而该过滤器的执行逻辑就是直接向routeHost参数的物理地址发起请求,从源码中我们可以知道该请求是直接通过httpclient包实现的,而没有使用Hystrix命令进行包装,所以这类请求并没有线程隔离和断路器的保护。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

知道配置类似zuul.routes.user-service.url=http://localhost:8080/这样的底层都是通过httpclient直接发送请求的,也就知道为什么这样的情况没有做到负载均衡的原因所在。

8,SendForwardFilter

它的执行顺序是500,是route阶段第三个执行的过滤器。该过滤器只对请求上下文中存在的forward.do参数进行处理请求,即用来处理路由规则中的forward本地跳转装配。

在这里插入图片描述

后置过滤器
9,SendErrorFilter

它的执行顺序是0,是post阶段的第一个执行的过滤器。该过滤器仅在请求上下文中包含error.status_code参数(由之前执行的过滤器设置的错误编码)并且还没有被该过滤器处理过的时候执行。而该过滤器的具体逻辑就是利用上下文中的错误信息来组成一个forward到api网关/error错误端点的请求来产生错误响应。

在这里插入图片描述

10,SendResponseFilter

它的执行顺序为1000,是post阶段最后执行的过滤器,该过滤器会检查请求上下文中是否包含请求响应相关的头信息,响应数据流或是响应体,只有在包含它们其中一个的时候执行处理逻辑。

而该过滤器的处理逻辑就是利用上下文的响应信息来组织需要发送回客户端的响应内容。

3,禁用过滤器

Spring Cloud默认为Zuul编写并启用了一些过滤器,一些场景下,想要禁用掉部分过滤器,此时该怎么办呢?

只需设置zuul.<SimpleClassName>.<filterType>.disable=true,即可禁用SimpleClassName所对应的过滤器。

以过滤器org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter为例,只需设置zuul.SendResponseFilter.post.disable=true,即可禁用该过滤器。

4,Zuul自定义过滤器

首先要自定义一个过滤器,只需要完成以下几个步骤:

  1. 继承ZuulFilter
  2. 指定过滤类型、过滤顺序
  3. 是否执行这个过滤器、过滤内容
1,首先定一个抽象类 AbstractZuulFilter.java 继承ZuulFilter
public abstract class AbstractZuulFilter extends ZuulFilter {

    protected RequestContext context;

    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        return (boolean) (ctx.getOrDefault(ContantValue.NEXT_FILTER, true));
    }

    @Override
    public Object run() {
        context = RequestContext.getCurrentContext();
        return doRun();
    }

    public abstract Object doRun();

    public Object fail(Integer code, String message) {
        context.set(ContantValue.NEXT_FILTER, false);
        context.setSendZuulResponse(false);
        context.getResponse().setContentType("text/html;charset=UTF-8");
        context.setResponseStatusCode(code);
        context.setResponseBody(String.format("{\"result\":\"%s!\"}", message));
        return null;
    }

    public Object success() {
        context.set(ContantValue.NEXT_FILTER, true);
        return null;
    }
}
2,定义preFilter的抽象类,继承AbstractZuulFilter。指定过滤器类型为pre类型。
public abstract class AbstractPreZuulFilter extends AbstractZuulFilter {
    @Override
    public String filterType() {
        return FilterType.pre.name();
    }
}
3,接着编写具体一个具体的限流过滤器
public class RateLimiterFilter extends AbstractPreZuulFilter {

    private static final Logger LOGGER = LoggerFactory.getLogger(RateLimiterFilter.class);

    /**
     * 每秒允许处理的量是50
     */
    RateLimiter rateLimiter = RateLimiter.create(50);

    @Override
    public int filterOrder() {
        return FilterOrder.RATE_LIMITER_ORDER;
    }

    @Override
    public Object doRun() {
        HttpServletRequest request = context.getRequest();
        String url = request.getRequestURI();
        if (rateLimiter.tryAcquire()) {
            return success();
        } else {
            LOGGER.info("rate limit:{}", url);
            return fail(401, String.format("rate limit:{}", url));
        }
    }
}
4,最后创建一个配置类,将下列过滤器托管给spring
@Configuration
public class ZuulConfigure {
  /**
   * 自定义过滤器
   * @return
   */
  @Bean
  public ZuulFilter rateLimiterFilter() {
    return new RateLimiterFilter();
  }
}

五,Zuul 容错与回退

很多时候,无论是因为服务节点的重启,宕机还是由于网络故障,我们在访问某一个服务节点时可能会出现阻塞或者异常。

Zuul进行路由时候也会因为这些原因出现异常。

此时如果我们直接将异常信息展示给用户的话肯定是很不友好的,我们需要展示给用户的是用户能看的明白的造成访问失败的原因。

这里我们就可以用到Zuul的回退处理了。

SpringCloud中使用Hystrix实现微服务的容错与回退,其实Zuul默认已经整合了Hystrix。

要实现Zuul添加回退,需要实现ZuulFallbackProvider接口,然后在实现类中,指定为哪个微服务提供回退,并提供一个ClientHttpResponse作为回退响应。

@Component
public class ZuulFallBack implements FallbackProvider {

  /**为哪个服务提供回退,*号代表所有服务**/
  @Override public String getRoute() {
    return "order-service"; //根据服务id指定为哪个微服务提供回退,可以用* 或者 null 代表所有服务//
  }
  /**回退响应**/
  @Override
  public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
    return new ClientHttpResponse() {

      /**回退时的状态码**/
      @Override
      public HttpStatus getStatusCode() throws IOException {
        //请求网关成功了,所以是ok
        return HttpStatus.OK;
      }

      /**数字类型状态码**/
      @Override
      public int getRawStatusCode() throws IOException {
        return HttpStatus.OK.value();
      }

      /**状态文本**/
      @Override
      public String getStatusText() throws IOException {
        return HttpStatus.OK.getReasonPhrase();
      }

      /****/
      @Override
      public void close() {

      }

      /**响应体**/
      @Override
      public InputStream getBody() throws IOException {
        JSONObject json =new JSONObject();
        json.put("state","501");
        json.put("msg","后台接口错误");
        //返回前端的内容
        return new ByteArrayInputStream(json.toJSONString().getBytes("UTF-8"));
      }

      /**返回的响应头**/
      @Override
      public HttpHeaders getHeaders() {
        HttpHeaders httpHeaders = new HttpHeaders();
        //设置头 return httpHeaders;
        httpHeaders.setContentType(MediaType.APPLICATION_JSON);
      }
    };
  }
}

注意:在Spring cloud Edgware版本之前,要想回退,需实现ZuulFallBackProvider接口,从Spring cloud Edgware版本之后,实现FallbackProvider接口。

六,写在最后

本文所用到的例子代码均已上传码云,传送门:

https://gitee.com/songbozhao/dashboard/projects

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐