Spring Cloud Zuul服务网关

一、前言

在微服务的架构中,服务网关就是一个介于客户端与服务端之间的中间层。在这种情况下,客户端只需要跟服务网关交互,无需调用具体的微服务接口。这样的好处在于,客户端可以降低复杂性;对于需要认证的服务,只需要在服务网关配置即可;同样也方便后期微服务的变更和重构,即微服务接口变更只需在服务网关调整配置即可,无需更改客户端代码。
Zuul是一款由Netflix开发的微服务网关开源软件,可以和其自家开发的Eureka,Ribbon和Hystrix配合使用,Spring Cloud对其进行了封装。

二、介绍

2.1 API 网关使用场景

1>黑白名单: 实现通过 IP 地址控制请求的访问
2>日志:实现访问日志的记录,进而实现日志分析,处理性能指标等
3>协议适配:实现通信协议的校验、适配转换的功能
4>身份认证:对请求进行身份认证
5>计流限流:可以设计限流规则,记录访问流量
6>路由:将请求进行内部(服务)转发

2.2 API 网关的实现

业界常用的 API 网关有很多方式,如:Spring Cloud Zuul、 Nginx、Tyk、Kong。本篇介绍的对象正是 Spring Cloud Zuul。
Zuul 是 Netflix 公司开源的一个 API 网关组件,提供了认证、鉴权、限流、动态路由、监控、弹性、安全、负载均衡、协助单点压测等边缘服务的框架。
Spring Cloud Zuul 是基于 Netflix Zuul 的微服务路由和过滤器的解决方案,也用于实现 API 网关。其中,路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入门的基础。而过滤功能是负责对请求的处理过程进行干预,是实现请求校验、服务聚合等功能的基础。
Spring Cloud Zuul 和 Eureka 进行整合时,Zuul 将自身注册到 Eureka 服务中,同时从 Eureka 中获取其他微服务信息,以便请求可以准确的通过 Zuul 转发到具体微服务上。

三、实战演练

本次测试案例基于之前发表的文章中介绍的案例进行演示,不清楚的读者请先转移至 《Spring Cloud Hystrix服务容错》 进行浏览。
当前的项目列表如下:

|项目 |服务实例 |实例id |端口 |描述 |
|:--|
|common-api |无 |无 |无 |公用的 api,如:实体类 |
|eureka-server |Eureka-Server |eureka-server1 |9001 |注册中心(Eureka 服务端) |
|eureka-server |Eureka-Server |eureka-server2 |9002 |注册中心(Eureka 服务端) |
|goods-server |Goods-Server |goods-server1 |8801 |商品服务(Eureka 客户端) |
|goods-server |Goods-Server |goods-server2 |8802 |商品服务(Eureka 客户端) |
|goods-server |Goods-Server |goods-server3 |8803 |商品服务(Eureka 客户端) |
|order-server |Order-Web |order-web |8900 |订单服务(Eureka 客户端) |
|user-server |User-Server |user-server1 |8788 |用户服务(Eureka 客户端) |
|user-server |User-Server |user-server2 |8789 |用户服务(Eureka 客户端) |
|gateway-server |GateWay-Server |gateway-server |9100 |服务网关(Eureka 客户端) |
创建一个为名 gateway-server 的 Spring Boot 项目。

3.1 添加依赖

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

3.2 配置文件

server:
  port: 9100

spring:
  application:
    name: GateWay-Server

eureka:
  instance:
    instance-id: gateway-server
    prefer-ip-address: true
  client:
    register-with-eureka: true     # 是否向注册中心注册自己
    fetch-registry: true           # 是否检索服务
    service-url:
      #defaultZone: http://127.0.0.1:8761/eureka/
      defaultZone:  http://eureka01:9001/eureka/ ,http://eureka02:9002/eureka/

3.3 启动 Zuul

在启动类上添加 @EnableZuulProxy 注解:

@EnableZuulProxy
@SpringBootApplication
public class GateWayServerApplication {

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

}

启动上边的所有项目,使用idea的http工具请求订单下单接口
图中,我们首先不经过网关直接访问 order-server 项目请求地址: http://localhost:8900/order/hystrix
image.png
之后再修改成访问 gateway-server 项目的请求地址: http://localhost:9100/order-web/order/hystrix
image.png
最终,响应结果都一样。
提示http://localhost:9100/order-web/order/hystrix 中第一个 order-web 表示的是注册在 Eureka 上的订单服务名称。

3.4 zuul 常用配置

传统配置

传统配置就是手动指定服务的转发地址,如在yml中配置:

zuul:
  routes:
    userweb:
      path: /userweb/**
      url: http://localhost:8765

通过上面的配置,所有符合/userweb/**规则的访问都将被路由转发到 http://localhost:8765 地址上,即当我们向服务网关访问 http://localhost:9100/userweb/userFeign/user/1 请求的时候,请求将被转发到 http://localhost:8765/userFeign/user/1 服务上.
image.png

基于服务名称配置

zuul:
  sensitive-headers:
  routes:
    wno704:
      path: /orderGateWay/**
      service-id: order-web

将订单服务的路由名称改成 orderGateWay。
使用 http 请求 http://localhost:9100/orderGateWay/order/hystrix 下单接口,运行结果:
image.png
请求成功。
基于服务名称的路由配置还可以进行简化,格式为zuul.routes.=,所以我们可以将orderGateWay的路由配置修改为:

zuul:
  routes:
    order-web:
      path: /orderGateWay/**

默认路由配置规则

我们尝试访问 http://localhost:9100/user-web/userFeign/user/1
image.png
我们压根没配置这个路由,为什么可以正常响应??其实Zuul配合Eureka后将会成一套默认的配置规则。当我们使用服务名称作为请求的前缀路径时,实际上就会匹配上类似下面的默认路由配置:

zuul:
  routes:
    user-web:
      path: /user-web/**
      serviceId: user-web

如果不想启动这个默认配置,我们可以使用zuul.ignored-services配置来关闭,如在yml中加入如下配置之后,再次访问 http://localhost:9100/user-web/userFeign/user/1 将返回404:

zuul:
  ignored-services: user-web

优先级

假如某个请求路径可以和多个路由配置规则相匹配的话,Zuul根据匹配的先后顺序来决定最终使用哪个路由配置。比如:

zuul:
  routes:
    userFeign:
      path: /userFeign/**
      service-id: User-Web
    userFeign1:
      path: /userFeign/userFeign/**
      service-id: User-Web1

当我们访问 http://localhost:9100/userFeign/userFeign/user/1 的时候,userFeign和userFeign1的路由配置都可以匹配上,但由于userFeign先于userFeign1配置,所以最终生效的是userFeign的配置。
假如将userFeign和userFeign1的配置顺序调换,再次访问 http://localhost:9100/userFeign/userFeign/user/1 时将抛出异常,原因是不存在服务名为User-Web1的服务:

com.netflix.zuul.exception.ZuulException: Forwarding error
....
Caused by: com.netflix.client.ClientException: Load balancer does not have available server for client: User-Web1
...

禁用路由:

zuul:
  ignored-patterns:
  - /orderGateWay/order/place/**    #禁用路由

http://localhost:9100/orderGateWay/order/place 无法被正常路由到订单服务,响应返回 404。
image.png

本地跳转

Zuul网关除了支持将服务转发到各个微服务上之外,还支持将服务跳转到网关本身的服务上,比如现在yml中有如下一段配置:

zuul:
  routes:
    test2:
      path: /test2/**
      url: forward:/test

我们在gateway-server入口类中加上该REST服务:

@RestController
public class TestController {
    @GetMapping("/test/hello")
    public String hello() {
        return "hello zuul";
    }
}

当访问 http://localhost:9100/test2/hello 时,Zuul会从本地/test/hello获取服务。
image.png

前缀配置:

zuul:
  prefix: /api

所有请求中的 path 需要添加 api 前缀。如: http://localhost:9100/orderGateWay/order/hystrix 需要改成 http://localhost:9100/api/orderGateWay/order/hystrix

头部过滤:

在使用Zuul网关的时候你可能会遇到Cookie丢失的情况,这是因为默认情况下Zuul会过滤掉HTTP请求头中的一些敏感信息,这些敏感信息通过下面的配置设定:

zuul:
  sensitive-headers: Cookie,Set-Cookie,Authorization # 设置全局敏感头,如果为空,表示接收所有敏感头信息

zuul:
  routes:
    wno704:
      path: /orderGateWay/**
      service-id: order-web
      sensitive-headers: Cookie,Set-Cookie,Authorization # 针对 /orderGateWay/ 的请求设置敏感头信息

使用Zuul另一个常见问题是重定向的问题,可以通过下面的设置解决:

zuul:
  add-host-header: true

四、核心过滤器

Spring Cloud Zuul为各个生命周期阶段实现了一批过滤器,这些过滤器的优先级和作用如下表所示:

|生命周期 |优先级 |过滤器 |功能描述 |
|:--|
|pre |-3 |ServletDetectionFilter |标记处理Servlet的类型 |
|pre |-2 |Servlet30WrapperFilter |包装HttpServletRequest请求 |
|pre |-1 |FormBodyWrapperFilter |包装请求体 |
|route |1 |DebugFilter |标记调试标志 |
|route |5 |PreDecorationFilter |处理请求上下文供后续使用 |
|route |10 |RibbonRoutingFilter |serviceId请求转发 |
|route |100 |SimpleHostRoutingFilter |url请求转发 |
|route |500 |SendForwardFilter |forward请求转发 |
|post |0 |SendErrorFilter |处理有错误的请求响应 |
|post |1000 |SendResponseFilter |处理正常的请求响应 |
其中优先级数字越小,优先级越高。
要关闭这些过滤器可以在application.yml中按照格式zuul...disable=true配置即可。比如关闭SendResponseFilter过滤器:

zuul:
  SendResponseFilter:
    post:
      disable:
        true

五、Zuul 自定义过滤器

Zuul 的核心技术就是过滤器,该框架提供了 ZuulFilter 接口让开发者可以自定义过滤规则。
我们以身份检验为例,自定义 ZuulFilter 过滤器实现该功能。

5.1 改造user-server

登录接口:
LoginController.java

@RestController
@RequestMapping("/user")
public class LoginController {
    @PostMapping("/login")
    public Result login(String username, String password, HttpServletResponse response) {


        if ("admin".equals(username) && "admin".equals(password)) {
            // 模拟生成 token,实际开发中 token 应存放在数据库或缓存中
            String token = "123456";
            Cookie cookie = new Cookie("token", token);
            cookie.setPath("/");
            cookie.setMaxAge(60 * 10);
            response.addCookie(cookie);

            return Result.success();
        }

        return Result.fail(401, "账号或密码错误");
    }
}

5.2 创建 ZuulFilter 过滤器

在 gateway-server 项目中,新建一个过滤器,需要继承 ZuulFilter 类:

@Component
public class AuthenticationFilter extends ZuulFilter {

    /**
     * 过滤器类型
     */
    @Override
    public String filterType() {
        return "pre";
    }

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

    /**
     * 是否开启过滤
     */
    @Override
    public boolean shouldFilter() {
        RequestContext context = RequestContext.getCurrentContext();
        HttpServletRequest request = context.getRequest();

        boolean flag = request.getRequestURI().contains("/login");
        // 如果是登录请求不进行过滤
        if (flag) {
            System.out.println("========不执行 zuul 过滤方法=======");
        } else {
            System.out.println("========执行 zuul 过滤方法=======");
        }
        return !flag;
    }

    /**
     * 过滤器执行内容
     */
    @Override
    public Object run() throws ZuulException {
        RequestContext context = RequestContext.getCurrentContext();
        HttpServletRequest request = context.getRequest();
        String token = request.getParameter("token");
        // 此处模拟获取数据库或缓存中的 token
        String dbToken = "123456";
        // 此处简单检验 token
        if (token == null || "".equals(token) || !dbToken.equals(token)) {
            context.setSendZuulResponse(false);
            context.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
        }

        return null;
    }
}

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

5.3 测试过滤器

运行所有项目,测试操作步骤如下:
测试效果图如下:
请求用户服务的登录接口( http://localhost:9100/api/user/user/login ),请求不执行 zuul 过滤方法,并且请求响应返回的 cookie 包含 token
image.png
请求订单服务的下单接口( http://localhost:9100/api/orderGateWay/order/hystrix ),但不携带 token,请求需要执行 zuul 过滤方法,请求响应 401 权限不足
image.png
请求订单服务的下单接口( http://localhost:9100/api/orderGateWay/order/hystrix ),携带之前登录接口返回的 token,请求需要执行 zuul 过滤方法,校验通过后路由到订单服务执行之后的操作
image.png

posted on 2026-08-10 15:29  爱河  阅读(1)  评论(0)    收藏  举报

导航