掘金 后端 ( ) • 2024-03-14 23:32

theme: channing-cyan

网关

网关,对内,是内部服务访问外部网站的出口;对外,是外部网站访问内部资源的入口。

网关就像一个管家,作为内外服务的一道屏障,可以很好的保护内部服务资源的安全,流量监控、统一认证、避免直接暴露内部服务信息等。

网关工作原理

可以把网关看作一道屏障,外部请求先经过网关,然后由网关转发至后端服务,后端服务处理后,将响应结果经网关返回给调用方:

image.png

流量统一收口到网关,我们可以利用网关做很多通用的事情,比如:

  1. 权限认证、解析
  2. 指标收集
  3. ...

日益成熟的 web 市场,现成的网关组件有很多,比如常见的 Spring Cloud Gateway、Netflix 的 zull 网关等。

网关的基本设计思路:

  1. 路由:能够将请求转发至对应的服务,这是网关的核心能力,转发规则可以多种多样
  2. 过滤器:网关需要易于扩展的 filter、可插拔式的,这里就会用到常见的责任链模式。

本文主要讲解 web 服务请求过程中经常使用的网关 zull 相关原理和实践。

zull 网关

基本配置

一份常见的 zull 网关配置:

server:
  port: 8080

spring:
  application:
    name: zuul-gateway

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

zuul:
  prefix: /api
  ignored-services: '*'
  routes:
    service1:
      path: /service1/**
      serviceId: service1
      stripPrefix: false
    service2:
      path: /service2/**
      serviceId: service2
      stripPrefix: false

设置了所有路由的前缀为/api,然后我们设置了 ignored-services为'*',这意味着 Zuul 将不会自动创建 Eureka 服务的路由。

  1. 然后定义了两个路由规则,所有以 /service1/ 开头的请求都会被路由到 service1 服务,所有以 /service2/ 开头的请求都会被路由到 service2 服务。
  2. stripPrefix 设置为 false 意味着在路由请求时,不会去掉路径前缀。
  3. retryable 设置为 true 意味着当请求失败时,Zuul 会尝试重新发送请求。

默认规则

如果你没有在 Zuul 的配置中明确指定要转发的服务,那么 Zuul 默认会将所有从 Eureka 服务注册中心注册的服务都创建路由规则。

例如,如果你有一个名为 service1 的服务在 Eureka 注册中心注册,那么 Zuul 默认会创建一个如下的路由规则:

zuul:
  routes:
    service1:
      path: /service1/**
      serviceId: service1

这意味着所有以 /service1/ 开头的请求都会被路由到 service1 服务。

如果你不希望 Zuul 自动创建路由规则,你可以在配置中添加 ignored-services: '*',这会告诉 Zuul 忽略所有服务:

zuul:
  ignored-services: '*'

然后,你可以手动添加你希望Zuul转发的服务。例如:

zuul:
  routes:
    service1:
      path: /my-service1/**
      serviceId: service1

在这个例子中,所有以 /my-service1/ 开头的请求都会被路由到 service1 服务。

路径模式

在 Zuul 的配置中,可以为每个服务定义一个或多个路径模式,Zuul 会将匹配这些模式的请求转发到相应的服务。

例如,以下配置将所有以 /service1/ 开头的请求转发到 service1 服务,将所有以 /service2/ 开头的请求转发到 service2 服务:

zuul:
  routes:
    service1:
      path: /service1/**
      serviceId: service1
    service2:
      path: /service2/**
      serviceId: service2

你也可以为同一个服务定义多个路径模式,例如:

zuul:
  routes:
    service1:
      path: /service1/**,/api/service1/**
      serviceId: service1

在这个例子中,所有以 /service1/或/api/service1/ 开头的请求都会被转发到 service1 服务。

按 URL 转发

在一些特殊的场景,你可能想要隐藏后端接口,需要在网关按接口维度做映射,比如:前端接口:/front/getUser -> 后端接口:/user-service/baseInfo

Zull 没有做到这样细粒度,不过它的 Filter 机制很方便扩展。

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;

import javax.servlet.http.HttpServletRequest;

public class DynamicRouteFilter extends ZuulFilter {

    @Override
    public String filterType() {
        return "pre"; // 在请求被路由之前调用
    }

    @Override
    public int filterOrder() {
        return 0; // filter执行顺序,通过数字指定
    }

    @Override
    public boolean shouldFilter() {
        // 这里可以写逻辑判断,是否要过滤,本文true,永远过滤。
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        
        // ...
        Map<String, Object> routeMap;
        String uri = requestContext.getRequest().getRequestURI();
        
        // ...
        
        if(routeMap.contains(uri)) {
            ctx.setSendZuulResponse(true); // 对请求进行路由
            ctx.setResponseStatusCode(200);
            ctx.set("isSuccess", true);
        } else {
            ctx.setSendZuulResponse(false); // 不对其进行路由
            ctx.setResponseStatusCode(401); // 返回错误码
            ctx.setResponseBody("IP address is not allowed"); // 返回错误内容
            ctx.set("isSuccess", false);
        }
        return null;
    }
}

在以上 routeMap 中,你可以自定义前后端接口映射配置,然后在此处判断当前请求是否配置了映射关系,进而决定是转发还是响应错误码。

ZuulFilter 接口定义了 Zuul 过滤器的基本结构,包含两个方法:

  1. shouldFilter():这个方法决定了是否需要执行 run() 方法。如果返回 true,则执行run() 方法;如果返回 false,则不执行。
  2. run():这是过滤器的核心方法,当 shouldFilter() 返回 true 时,该方法会被调用。这个方法可以包含过滤器的具体逻辑,例如安全验证、限流控制等。如果在执行过程中发生错误,会抛出 ZuulException 异常。

生命周期

以一条请求为例,先看看一条网关的全生命周期:

  1. 预过滤(Pre Filter) :这是请求在进入 Zuul 路由之前执行的过滤,通常用于实现身份验证、记录调试信息、决定是否需要对请求进行路由等。
  2. 路由(Routing Filter) :在这个阶段,Zuul 会根据设定的规则将请求路由到对应的服务实例。
  3. 后过滤(Post Filter) :这是在路由到微服务后执行的过滤器,这个过滤器将会对返回的数据进行处理,比如添加 HTTP Header、收集统计和指标信息、将响应输出到客户端等。
  4. 错误处理(Error Filter) :在整个生命周期中任何阶段发生错误时都会进入该过滤器,该过滤器用于统一处理请求过程中出现的异常。
// Pre Filter
public class SimplePreFilter extends ZuulFilter {

  @Override
  public String filterType() {
    return "pre";
  }

  @Override
  public int filterOrder() {
    return 1;
  }

  @Override
  public boolean shouldFilter() {
    return true;
  }

  @Override
  public Object run() {
    RequestContext ctx = RequestContext.getCurrentContext();
    HttpServletRequest request = ctx.getRequest();

    log.info(String.format("%s request to %s", request.getMethod(), request.getRequestURL().toString()));

    return null;
  }
}

// Post Filter
public class SimplePostFilter extends ZuulFilter {

  @Override
  public String filterType() {
    return "post";
  }

  @Override
  public int filterOrder() {
    return 1;
  }

  @Override
  public boolean shouldFilter() {
    return true;
  }

  @Override
  public Object run() {
    RequestContext ctx = RequestContext.getCurrentContext();
    HttpServletRequest request = ctx.getRequest();

    log.info("Response Status : " + ctx.getResponseStatusCode());

    return null;
  }
}

// Error Filter
public class SimpleErrorFilter extends ZuulFilter {

  @Override
  public String filterType() {
    return "error";
  }

  @Override
  public int filterOrder() {
    return 1;
  }

  @Override
  public boolean shouldFilter() {
    return true;
  }

  @Override
  public Object run() {
    RequestContext ctx = RequestContext.getCurrentContext();
    HttpServletRequest request = ctx.getRequest();

    log.info("Error occurred, request to " + request.getRequestURL().toString());

    return null;
  }
}

以上例子中,定义了三个过滤器:预过滤器、后过滤器和错误过滤器。

  1. 预过滤器记录了请求的HTTP方法和URL
  2. 后过滤器记录了响应的状态码
  3. 错误过滤器记录了发生错误的请求URL。

以上便是 zull 网关的核心原理以及实践,欢迎交流探讨!