1. 概述

Zuul是一种提供动态路由、监视、弹性、安全性等功能的边缘服务,是一个基于JVM路由和服务端的负载均衡器,在Spring Cloud框架中,Zuul的角色是网关,负责接收所有REST请求,然后进行内部转发,是微服务提供者集群的流量入口。

1.1. 主要功能

路由:将不同REST请求转发至不同的微服务提供者,其作用类似于Nginx的反向代理。同时,也起到了统一端口的作用,将很多微服务提供者的不同端口统一到了Zuul的服务端口
认证:网关直接暴露在公网上时,终端要调用某个服务,通常会把登录后的token(令牌)传过来,网关层对token进行有效性验证。如果token无效(或没有token),就不允许访问REST服务。可以结合Spring Security中的认证机制完成Zuul网关的安全认证
限流:高并发场景下瞬时流量不可预估,为了保证服务对外的稳定性,限流成为每个应用必备的一道安全防火墙。如果没有这道安全防火墙,那么请求的流量超过服务的负载能力时很容易造成整个服务的瘫痪
负载均衡:在多个微服务提供者之间按照多种策略实现负载均衡

2. 搭建Zuul网关服务

Zuul作为网关层微服务,跟其他服务提供者一样都注册在Eureka Server上,可以相互发现。Zuul能感知到哪些服务Provider实例在线,同时通过配置路由规则可以将REST请求自动转发到指定的后端微服务提供者

2.1. 引入核心依赖

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

2.2. 编写主启动类

@EnableZuulProxy
@SpringBootApplication
public class ZuulApplication {

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

在启动类中添加注解@EnableZuulProxy,声明这是一个网关服务提供者

2.3. 编写application.yml基础配置

server:
  port: 8810
spring:
  application:
    name: cloud-zuul
eureka:
  client:
    register-with-eureka: false
    fetch-registry: true
    service-url:
      defaultZone: http://localhost:8761/eureka
  instance:
    prefer-ip-address: true
    ip-address: ${spring.cloud.client.ip-address}
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
zuul:
  routes:
    cloud-provider:
      path: /cloud-provider/**
      serviceId: cloud-provider

2.4. 验证

依次启动服务Eureka Server、Provider和Zuul,在浏览器地址输入http://localhost:8770/provider/getProviderInfo/world

微服务网关的作用与功能 微服务网关实现的功能_网关


再次在浏览器地址栏输入http://localhost:8810/cloud-provider/provider/getProviderInfo/world

微服务网关的作用与功能 微服务网关实现的功能_网关_02

3. 路由规则配置

路由规则通常有两种方式,其一是路由到直接URL,其二是路由到微服务提供者
两种方式的区别如下:

  • 第一种方式使用url属性来指定直接的上游URL的前缀,第二种方式使用serviceId属性来指定上游服务提供者的名称
  • 第二种方式需要结合Eureka Client客户端来实现动态的路由转发功能,需要配置Eureka相关配置信息

3.1. 过滤敏感请求头部

防止请求头泄露的方式之一是,在Zuul的路由配置中指定要忽略的请求头列表,并且多个敏感头部之间可以用逗号隔开,默认情况,Zuul转发请求会把header清空,如果在微服务集群内部转发请求,上游Provider就会收不到任何头部,如果需要传递原始的header信息到最终的上游,就需要添加如下敏感头部设置

zuul:
  routes:
    cloud-provider:
      sensitiveHeaders:

如何需要屏蔽头信息,需要如下配置

zuul:
  routes:
    cloud-provider:
      sensitiveHeaders: Cookie,Set-Cookie,token,backend,Authorization

3.2. 路径前缀处理

默认情况下Zuul会去掉路由的路径前缀,如果上游微服务提供者没有配置路径前缀,Zuul这种默认处理和转发就不会有问题,如果上游微服务提供者配置了统一的路径前缀,前缀去掉后,上游服务提供者就会报404错误,找不到URL对应的资源服务。可以设置配置项stripPrefix的值为false

zuul:
  routes:
    cloud-provider:
      path: /cloud-provider/**
      serviceId: cloud-provider
      stripPrefix: true #是否取消请求前缀

如果需要对访问网关的所有请求都加上前缀,可以设置配置prefix,具体配置如下:

zuul:
  prefix: /native #所有请求添加前缀
  routes:
    cloud-provider:
      path: /cloud-provider/**
      serviceId: cloud-provider
      stripPrefix: true #是否取消请求前缀
      sensitiveHeaders: Cookie,Set-Cookie,token,backend,Authorization

4. 查看路由信息

4.1. 引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

4.2. 开启路由监控端口

management:
  endpoints:
    web:
      exposure:
        include: 'routes' #开启查看路由端口

4.3. 验证

重新启动服务,在浏览器地址栏输入http://localhost:8810/actuator/routes

微服务网关的作用与功能 微服务网关实现的功能_微服务_03

5. 过滤器

通过定义过滤器来实现请求的拦截和过滤

5.1. 过滤器类型

pre类型过滤器
请求路由之前调用,用于实现身份验证、记录调试信息
route类型过滤器
发送请求到上游服务,例如使用Apache HttpClient或Netflix Ribbon请求上游服务
post类型过滤器
上游服务返回之后调用,为响应添加HTTP响应头、收集统计信息和指标、将响应回复给客户端
error类型过滤器
在其他阶段发生错误时执行

5.2. 请求处理流程

  1. 当外部请求到达Zuul网关时,首先会进入pre处理阶段,在这个阶段请求将被pre类型的过滤器处理,以完成再请求路由的前置过滤处理,比如请求的校验等。在完成pre类型的过滤处理之后,请求进入第二个阶段:route路由请求转发阶段
  2. 在route路由请求转发阶段,请求将被route类型的过滤器处理,route类型的过滤器将外部请求转发到上游的服务。当服务实例的结果返回之后,route阶段完成,请求进入第三个阶段:post处理阶段
  3. 在post处理阶段,请求将被post类型的过滤器处理,post类型的过滤器在处理的时候不仅可以获取请求信息,还能获取服务实例的返回信息,所以post阶段可以对处理结果进行一些加工或转换等
  4. 还有一个特殊的阶段error,在该阶段请求将被error类型的过滤器处理,在上述3个阶段发生异常时才会触发,但是error过滤器也能将最终结果返回给请求客户端

5.3. 实例

Zuul提供一个过滤器ZuulFilter抽象基类,可以作为自定义过滤器的父类,需要实现的方法主要有4个
filterType方法
返回自定义过滤器类型,以常量的形式定义在FilterConstants类中
filterOrder方法
返回过滤器顺序,值越小优先级越高
shouldFilter方法
返回过滤器是否生效,返回true表示生效,返回false表示不生效
run方法
过滤器业务逻辑处理,可以进行当前的请求拦截和参数定制,后续路由定制,返回结果定制
例如可以使用前置过滤器打印日志和黑名单处理过滤,具体代码如下

@Component
public class BlackListFilter extends ZuulFilter {

    private static final Logger logger = LoggerFactory.getLogger(BlackListFilter.class);
    static List<String> blackList = Arrays.asList("");

    /**
     * 过滤器类型pre:过滤之前;routing:路由之时;post:路由之后;error:发送错误调用
     * @return
     */
    @Override
    public String filterType() {
        return "pre";
    }

    /**
     * 过滤执行次序
     * @return
     */
    @Override
    public int filterOrder() {
        return 0;
    }

    /**
     * 是否执行过滤
     * @return
     */
    @Override
    public boolean shouldFilter() {
        RequestContext context = RequestContext.getCurrentContext();
        if (!context.sendZuulResponse()) {
            return false;
        }
        //返回true,表示需要执行run方法
        HttpServletRequest request = context.getRequest();
        if (request.getRequestURI().startsWith("/native")) {
            return true;
        }
        return false;
    }

    /**
     * 过滤器具体执行方法
     * @return
     * @throws ZuulException
     */
    @Override
    public Object run() throws ZuulException {
        RequestContext context = RequestContext.getCurrentContext();
        HttpServletRequest request = context.getRequest();
        String host = request.getRemoteHost();
        String method = request.getMethod();
        String uri = request.getRequestURI();
        logger.info("=====>Remote host:{},method:{},uri:{}", host, method, uri);
        String username = request.getParameter("username");
        if (null != username && blackList.contains(username)) {
            logger.info(username + " is forbidden: " + request.getRequestURL().toString());
            context.setSendZuulResponse(false);
            try {
                context.getResponse().setContentType("text/html;charset=utf-8");
                context.getResponse().getWriter().write("对不起,您已进入黑名单!");
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
        return null;
    }
}