Spring Cloud从入门到精通(六):网关服务 Zuul

Zuul

网关服务是整个微服务系统的入口,所有的外部客户端请求都需要经过它来调度和过滤,一般网关服务需要具有请求路由、负载均衡、权限认证、校验过滤、与服务治理框架结合等等功能。而Spring Cloud为我们提供了基于Netflix Zuul实现的Spring Cloud Zuul。

入门案例

请求路由

首先启动我们前几节搭建的服务注册中心和服务消费者。

(1)创建一个Zuul工程,名称为api-gateway,添加依赖

 <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
            <version>2.0.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            <version>2.0.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.0.0.RELEASE</version>
        </dependency>
    </dependencies>

(2)编写启动类,开启zuul注解

@EnableZuulProxy
@SpringCloudApplication
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class);
    }
}

 
 

(3)编写配置文件,路由的配置zuul.routes.xxx ,xxx是路由名字,自定义,我们需要配置该路由下的path和serviceId,这样就可以做到以path指定的前缀访问时映射到serviceId指定的服务。
ps:在这儿遇到一个大坑,我端口号随便打了个6666,然后怎么访问都是失败,浪费了一个小时,才知道chrome因为安全原因是禁用一些端口的。

spring:
  application:
    name: api-gateway
server:
  port: 9006
zuul:
  routes:
    api:
      path: /api/**
      serviceId: feign-consumer
eureka:
  client:
    service-url:
      defaultZone: http://server1:8001/eureka/

 
 

(4)访问http://localhost:9006/api/feign/testFeign。可见zuul做了负载均衡的操作。

在这里插入图片描述在这里插入图片描述

请求过滤

为了系统的一些安全性等方面的原因,我们是需要对一些请求进行过滤和安全校验的。而zuul的一个核心功能就是请求过滤。zuul允许在API网关上定义过滤器来实现对请求的拦截与过滤,实现的方法非常简单,我们来操作一下。

(1)继承ZuulFilter抽象类,实现自己的Filter,功能为判断请求参数中是否有accessToken,没有就拒绝访问。

public class AccessFilter extends ZuulFilter {
	//过滤器类型,决定过滤器在哪个生命周期进行。返回"pre"代表会在路由之前进行过滤
    @Override
    public String filterType() {
        return "pre";
    }
	//过滤器的执行顺序。当有多个过滤器时,需要根据该方法的返回值来依次执行。
    @Override
    public int filterOrder() {
        return 0;
    }
	//判断该过滤器是否要执行.返回true对所有请求都要执行。
    @Override
    public boolean shouldFilter() {
        return true;
    }
	//过滤器的具体逻辑,通过setSendZuulResponse(false)来令zuul过滤该请求。
    @Override
    public Object run() throws ZuulException {
        RequestContext currentContext = RequestContext.getCurrentContext();
        HttpServletRequest request = currentContext.getRequest();
        String accessToken = request.getParameter("accessToken");
        if(accessToken==null){
            currentContext.setSendZuulResponse(false);
            currentContext.setResponseStatusCode(909);
            return null;
        }
        return null;
    }
}

 
 

(2)创建具体的过滤器Bean

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

 
 

(3)重新启动
访问http://localhost:9006/api/feign/testZuul,可以看到无内容并且返回失败状态码为我们定义的909.

在这里插入图片描述

访问http://localhost:9006/api/feign/testZuul?accessToken=111,则能正常的进行访问。

在这里插入图片描述

路由详解

我们在前面进行路由配置时,采用了如下的配置。

zuul:
  routes:
    api:
      path: /api/**
      serviceId: feign-consumer

 
 

zuul为我们提供了更为简洁的配置。

zuul.routes.<serviceId>=<path>
例如:
zuul:
  routes:
    feign-consumer: /api/**

 
 

路由匹配

zuul中路由匹配的路径表达式采用了Ant风格。

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

 
 

当我们使用通配符时,经常会出现这样的问题,一个URL路径会被多个路由规则匹配上。例如/user-service/** 和 /user-service/ext/** 。

那当我们调用user-service-ext的服务时,就会被上面两个路由规则所匹配。我们需要让其优先选择匹配/user-service/ext/**,然后再匹配/user-service/**。

根据zuul的路由匹配算法,它内部是通过线性遍历的方式,在请求路径获取到第一个匹配的路由规则后就返回并结束匹配过程。所以当存在多个匹配的路由规则时,匹配结果取决于路由规则的保存顺序,路由规则的保存顺序是通过遍历配置文件中路由规则依次加入的,也就是路由规则定义的越靠前,就越优先被匹配到。

忽略表达式

为了能够更加灵活地配置路由规则,zuul还提供了一个忽略表达式参数zuul.ignored-patterns,该参数可以用来设置不希望被网关进行路由的URL路径。

以前面为例,我们不希望feign接口被路由。
zuul:
  routes:
    feign-consumer: /api/**
  ignored-services: '*'
  ignored-patterns: /**/feign/**

 
 

访问http://localhost:9006/api/feign/simple,发现访问失败。
注意该参数是一个表达式,所有符合表达式的都会被忽略。

在这里插入图片描述

路由的默认规则

zuul其实会为每个服务自动创建一个默认的路由规则,这个默认规则指定path使用serviceId作为前缀,所以我们还可以这样访问前面的例子。

http://localhost:9006/feign-consumer/feign/test

在这里插入图片描述

由于zuul会自动对所有服务创建默认的路由规则,这就会让一些我们不需要被外界使用的服务被访问到。这时候,我们可以使用zuul.ignored-services来设置一个匹配表达式来对符合规则的服务不创建默认的路由规则。zuul会在自动创建服务路由的时候根据该表达式来进行判断,如果服务名匹配,则跳过不为其创建路由规则。
例如,当zuul.ignored-services=*的时候,就对所有服务都不自动创建路由规则,我们测试一下。

zuul:
  routes:
    feign-consumer: /api/**
  // *一定要加上单引号,要不然解析报错
  ignored-services: '*'

 
 

这时候我们使用默认的路由规则访问就会报错,只能按照我们配置的路由规则进行访问。
在这里插入图片描述

路由前缀

Zuul提供了zuul.prefix参数来为全局的路由规则设置前缀。

zuul:
  routes:
    feign-consumer: /api/**
  ignored-services: '*'
  prefix: /test

 
 

在这里插入图片描述

zuul默认在请求路由的时候是会把设置的全局前缀给移除的,我们可以通过zuul.strip-prefix=false不让它移除。这样当我们访问http://localhost:9006/feign/simple到网关服务的时候,网关服务将请求路由到feign-consumer服务的时候,就会把feign前缀给带上。

zuul:
  routes:
    feign-consumer: /*
  ignored-services: '*'
  prefix: /feign
  strip-prefix: false

 
 

在这里插入图片描述

cookie与头信息

默认情况下,Spring Cloud Zuul在请求路由时,会过滤掉HTTP请求头信息中的一些敏感信息,敏感头信息通过zuul.sensitive-headers参数定义,默认的为"Cookie", “Set-Cookie”, "Authorization"三个,那么,我们常用的cookie默认就是不会传递的,这就会引发问题,由于cookie无法传递,我们web应用将无法实现登录和鉴权。
为了解决这个方法,有以下几种解决方案。

(1)把zuul.sensitive-headers设置为空,覆盖默认设置,不推荐。

(2)设置zuul.routes.< route >.customSensitiveHearders ,对指定路由设置自定义敏感头信息。

(3)设置zuul.routes.< route >.sensitive-headers为空,对指定路由设置敏感头信息为空。

由于zuul整合了Hystrix和Ribbon,所以使用zuul就可以使用Hystrix和Ribbon的各种配置。
ribbon的超时时间设置最好要小于Hystrix的命令超时时间,这样当ribbon请求超时,会自动进行重试,如果ribbon超时时间大于Hystrix的命令超时时间,这时候出现请求超时,会直接返回请求失败,不会进行重试,只要Ribbon的超时时间小于Hystrix的超时时间,就会进行重试,当然是要在开启重试的前提下。

hystrix:
  command:
    defualt:
      execution:
        isolation:
          thread:
          	//设置Hystrix命令超时时间
            timeoutInMilliseconds: 5000
ribbon:
  //这个为建立连接后从服务器读取可用资源的超时时间
  ReadTimeout: 2000
  //这个为建立连接的超时时间
  ConnectionTimeout: 2000
  //重试次数
  MaxAutoRetries: 1
  //对所有操作都执行重试操作
  OkToRetryOnAllOperations: true

 
 

过滤器详解

过滤器是zuul实现网关服务的最为核心的部件,其实在请求路由功能运行时,它也是依靠过滤器完成。首先会通过pre类型的过滤器将请求路径和路由规则进行匹配,以找到需要转发的目标地址。然后再由route类型的过滤器来完成请求转发,所以每一个进入zuul的HTTP请求都会经过一系列的过滤器处理链得到请求响应并返回客户端。

zuul中的过滤器必须包含四个基本特征,也就是ZuulFilter接口中定义的四个抽象方法,分别是过滤类型、执行顺序、执行条件、执行逻辑。

过滤类型filterType:返回一个字符串代表过滤类型,zuul默认定义了四种。

  • pre:可以在请求被路由之前调用
  • routing:在路由请求时被调用
  • error:处理请求时发生错误时被调用
  • post:在routing和error过滤器之后被调用

执行顺序filterOrder:返回一个整数来定义过滤器的执行顺序,数值越小优先级越高。

执行条件shouldFilter:返回一个boolean值判断该过滤器是否要执行,可以通过该方法指定过滤器的有效范围。

执行逻辑run:过滤器的具体逻辑

请求生命周期

![请求生命周期](https://img-blog.csdnimg.cn/2020122310154859.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDE5MDA4Nw==,size_16,color_FFFFFF,t_70#pic_center)


zuul是通过Servlet来实现的,Zuul通过自定义的ZuulServlet(类似于SpringMVC的DispatcherServlet)来对请求进行控制,ZuulServlet的作用是初始化ZuulFilter,并且指定ZuulFilter的执行顺序。

    public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
        try {
            init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
            // Marks this request as having passed through the "Zuul engine", as opposed to servlets
            // explicitly bound in web.xml, for which requests will not have the same data attached
            RequestContext context = RequestContext.getCurrentContext();
            context.setZuulEngineRan();
            try {
                preRoute();
            } catch (ZuulException e) {
                error(e);
                postRoute();
                return;
            }
            try {
                route();
            } catch (ZuulException e) {
                error(e);
                postRoute();
                return;
            }
            try {
                postRoute();
            } catch (ZuulException e) {
                error(e);
                return;
            }
        } catch (Throwable e) {
            error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
        } finally {
            RequestContext.getCurrentContext().unset();
        }
    }

从代码可以看出,当外部Http请求到达zuul时,首先会进入第一个阶段pre,在这里会被pre类型的过滤器进行处理,主要是在请求路由之前做一些前置加工,比如请求校验等,之后进入第二个阶段routing,也就是路由请求转发阶段,请求会被routing类型的过滤器进行处理,主要是把外部请求转发到具体的服务实例上,当服务实例将请求结果都返回以后,routing阶段完成,进入第三个阶段post,此时请求会被post类型的过滤器处理,这些过滤器在处理的时候不仅可以获取到请求信息,还能获取到服务实例的返回信息,所以我们可以在post类型的过滤器中对结果进行一些加工。此外,还有一个特殊的阶段error,该阶段在上述三个阶段发生异常时触发,但是最终的流向还是post类型的过滤器,因为需要通过post过滤器将最终结果返回给客户端。

zuul在HTTP请求生命周期的各个阶段默认实现了一批核心过滤器,它们会在网关服务启动时被自动加载和启用,它们定义于org.springframework.cloud.netflix.zuul.filters包下,我们来了解一下可以帮助我们理解zuul对外部请求处理的过程。

pre过滤器

  • ServletDetectionFilter:执行顺序为-3.是最先被执行的过滤器。该过滤器总是会被执行,主要用来检测当前请求是通过Spring的DispatcherServlet处理运行的,还是通过ZuulServlet来处理运行的。它的检测结果会以boolean类型保存在当前请求上下文的isDispatcherServletRequest参数中,这样在后续的过滤器中,我们就可以通过RequestUtils.isDispatcherServletRequest()和RequestUtils.isZuulServletRequest()方法来判断请求处理的源头,以实现不同的处理机制。一般情况下,发送到zuul的请求都会被Spring的DispatcherServlet处理,除了通过/zuul/*路径访问的请求会绕过DispatcherServlet,被ZuulServlet处理,主要用来应对大文件上传的情况。另外,对于ZuulServlet的访问路径/zuul/*,我们可以通过zuul.servletPath参数修改。

  • Servlet30WrapperFilter:执行顺序为-2.是第二个执行的过滤器,主要是为了将原始的HttpServletRequest包装成Servlet30RequestWrapper对象。

  • FormBodyWrapperFilter:执行顺序为-1.是第三个执行的过滤器。该过滤器只对两类请求生效,第一类是Content-Type为application/x-www-form-urlencode的请求,第二类是Content-Type为multipart/form-data并且是由Spring的DispatcherServlet处理的请求。该过滤器的作用是将符合要求的请求体包装成FormBodyRequestWrapper对象。

  • DebugFilter:执行顺序为1,是第四个执行的过滤器。该过滤器会根据配置参数zuul.debug.request和请求中的debug参数来决定是否执行过滤器中的操作。它具体操作是将当前请求上下文中的debugRouting和debugRequest参数设置为true,由于在同一个请求的不同生命周期阶段都可以访问到这两个值,所以在后面的过滤器中可以利用这两个值来定义一些debug信息。这样当出现问题时,可以通过请求参数的方式激活这些debug信息帮助分析问题。

  • PreDecorationFilter:执行顺序为5.是pre阶段最后被执行的过滤器。该过滤器会判断当前请求上下文中是否存在forward.to和serviceId参数,如果都不存在,就会执行过滤器的逻辑,如果有一个存在的话,说明当前请求已经被处理过了,因为这两个信息就是根据当前请求的路由信息加载进来的。它的具体操作就是为当前请求做一些预处理,例如进行路由规则的匹配、在请求上下文中设置该请求的基本信息等,这些信息是后续过滤器进行处理的重要依据,我们可以通过RequestContext.getCurrentContext()来访问这些信息, 并且它还会对一些请求头做一些设置,例如X-Forwarded-Host、X-Forwarded-Port等,对于这些请求头是通过zuul.addProxyHeaders参数进行设置的,默认为true,所以zuul在请求跳转时会默认添加X-Forwarded-*请求头。

route过滤器

  • RibbonRoutingFilter:执行顺序为10.是route阶段第一个执行的过滤器,该过滤器只对请求上下文中存在serviceId参数的请求进行处理,即对通过serviceId配置路由规则的请求生效,它通过使用Ribbon和Hystrix来向实例发起请求,并将请求结果返回。
  • SimpleHostRoutingFilter:执行顺序为100.是route阶段第二个执行的过滤器。该过滤器只对请求上下文中存在routeHost参数的请求进行处理,即只对通过url配置路由规则的请求生效。这种做法特别落后,所以我们就没讲,我们仅仅使用serviceId那种就行。
  • SendForwardFilter:执行顺序为500.是route阶段第三个执行的过滤器,该过滤器只对请求上下文中存在forward.to参数的请求进行处理,即用来处理路由规则中的forward本地跳转。

post过滤器

  • SendErrorFilter:执行顺序为0.是post阶段第一个执行的过滤器.该过滤器仅在请求上下文中包含error.status_code参数(由之前执行的过滤器设置的错误编码)并且还没有被该过滤器处理过的时候执行,该过滤器的具体逻辑就是利用请求上下文的错误信息来组成一个forward到服务网关/error错误端点的请求来产生错误响应。
  • SendResponseFilter:执行顺序为1000。是post阶段最后执行的过滤器,该过滤器会检查请求上下文中是否包含请求响应相关的头信息、响应数据流或是响应体,只有在包含它们其中一个的时候执行。它利用请求上下文的响应信息来组织需要发送回客户端的响应内容。

禁用过滤器
zuul指定了一个参数来禁用指定的过滤器。

//className为过滤器的类名,filterType为过滤器的类型
zuul.<className>.<filterType>.disable=true
例如:zuul.AccessFilter.pre.disable=true

 
 

该参数可以禁用zuul默认定义的核心过滤器。

Zuul是采用了类似SpringMVC的DispacherServlet的ZuulServlet来实现的,采用的是异步阻塞模型,性能比nginx差,但是zuul的横向扩展能力很好,可以很方便的构建集群。
在实际的集群架构中,通常是通过Nginx和Zuul相互结合来做负载均衡,暴露在最外面的是Nginx主从双热备进行Keepalive,Nginx经过某种路由策略,将请求路由转发到Zuul集群,Zuul最终将请求分发到具体的服务。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Cloud 是一个基于 Spring Boot 的微服务框架,它提供了一套完整的解决方案,用于构建分布式系统和微服务架构。如果你想从入门到精通,可以按照以下步骤进行学习: 1. 首先,你需要掌握 Spring Boot 的基础知识,包括 Spring Boot 的核心概念、注解和常用配置等。你可以通过官方文档、教程或者相关书籍来学习。 2. 掌握 Spring Cloud 的核心组件,比如服务注册与发现(Eureka、Consul、Zookeeper)、负载均衡(Ribbon、LoadBalancer)、熔断器(Hystrix)、配置中心(Config Server)等。了解每个组件的原理和用法,并能够在实际项目中进行配置和使用。 3. 学习微服务架构的设计原则和最佳实践,包括服务拆分、服务间通信、数据一致性、安全性等。了解分布式系统的挑战和解决方案。 4. 深入学习 Spring Cloud 的扩展组件,比如服务网关Zuul、Gateway)、消息总线(Spring Cloud Bus)、链路追踪(Sleuth)、分布式配置中心(Spring Cloud Config)等。这些组件可以帮助你构建更加复杂和高可用的微服务系统。 5. 实践项目开发,通过实际的项目实践来巩固所学知识。可以选择一些开源的项目或者自己构建一个小型的微服务应用来练手。 6. 关注 Spring Cloud 生态圈的最新动态和技术发展,参加相关的技术交流和分享活动,与其他开发者进行交流和学习。 总之,学习 Spring Cloud 需要一定的时间和经验积累,通过不断的实践和学习,你可以逐渐从入门到精通。希望这些步骤对你有所帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值