SpringCloud(三) Zuul

西涼馬戳戳發表於2021-12-26

Zuul

有了eureka 、 feign 和 hystrix 後,基本上就搭建了簡易版的分散式專案,但仍存在一些問題,比如:

1、如果我們的微服務中有很多個獨立服務都要對外提供服務,那麼我們要如何去管理這些介面?特別是當專案非常龐大的情況下要如何管理?

2、在微服務中,一個獨立的系統被拆分成了很多個獨立的服務,為了確保安全,許可權管理也是一個不可迴避的問題,如果在每一個服務上都新增上相同的許可權驗證程式碼來確保系統不被非法訪問,那麼工作量也就太大了,而且維護也非常不方便。

所以出現了閘道器,它就像一個安檢站一樣,所有外部的請求都需要經過它的排程與過濾,然後 API 閘道器來實現請求路由、負載均衡、許可權驗證等功能。

使用 Zuul 構建 API 閘道器

  1. 建立spring boot工程並新增依賴:

    <!--新增 spring cloud 的 zuul 的起步依賴--> <dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
    </dependency>
    <!--新增 spring cloud 的 eureka 的客戶端依賴--> <dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    
  2. 在入口類上新增@EnableZuulProxy 註解,開啟 Zuul 的 API 閘道器服務功能:

    @SpringBootApplication
    @EnableEurekaClient
    @EnableZuulProxy
    public class SpringcloudZuulApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(SpringcloudZuulApplication.class, args);
        }
    
    }
    
  3. 在application.yml中新增路由規則

    # 配置路由規則
    zuul:
      routes:
        hello:
          path: /api-zuul/**
          serviceId: springcloud-consumer
    

    說明:以上配置的路由規則就是匹配所有符合/api-zuul/**的請求,只要路徑中帶有/api-zuul/都將被轉發到 springcloud-consumer 服務上。比如:localhost:8766/api-zuul/web/hello 轉發到 http://localhost:8764/web/hello

  4. 構建成功

使用 Zuul 進行請求過濾

  1. 定義一個過濾器類並繼承自 ZuulFilter,並將該 Filter 作為一個 Bean:

    @Component
    public class AuthFilter extends ZuulFilter {
        @Override
        public String filterType() {
            return "pre";
        }
    
        @Override
        public int filterOrder() {
            return 0;
        }
    
        @Override
        public boolean shouldFilter() {
            return true;
        }
    
        @Override
        public Object run() throws ZuulException {
            RequestContext     ctx     = RequestContext.getCurrentContext();
            HttpServletRequest request = ctx.getRequest();
            String             token   = request.getParameter("token");
            if (token == null) {
                ctx.setSendZuulResponse(false);
                ctx.setResponseStatusCode(401);
    
                ctx.addZuulResponseHeader("content-type","text/html;charset=utf-8");
                ctx.setResponseBody("非法訪問");
            }
            return null;
        }
    }
    
    • filterType 方法的返回值為過濾器的型別,決定了過濾器在哪個生命週期執行,pre 表示在路由之前執行過濾器,其他值還有 post、error、route 和 static,當然也可以自定義。
    • filterOrder 方法表示過濾器的執行順序,當過濾器很多時,我們可以通過該方法的返回值來指定過濾器的執行順序。
    • shouldFilter 方法用來判斷過濾器是否執行,true 表示執行,false 表示不執行。
    • run 方法則表示過濾的具體邏輯,如果請求地址中攜帶了 token 引數的話,則認為是合法請求,否則為非法請求,如果是非法請求的話,首先設定ctx.setSendZuulResponse(false),表示不對該請求進行路由,然後設定響應碼和響應值。這個 run 方法的返回值目前暫時沒有任何意義,可以返回任意值。
  2. 不攜帶token,localhost:8766/api-zuul/web/hello

  3. 攜帶token,localhost:8766/api-zuul/web/hello?token=213

Zuul 的路由規則

  1. 在前面的例子中,

    zuul:
      routes:
        hello:
          path: /api-zuul/**
          serviceId: springcloud-consumer
    

    當訪問地址符合 /api-zuul/ 規則的時候,會被自動定位到springcloud-consumer 服務上,有點麻煩,還可以簡化為:

    zuul:
      routes:
        springcloud-consumer: /api-zuul/**
    

    zuul.routes 後面跟著的是服務名,服務名後面跟著的是路徑規則,這種配置方式更簡單。

  2. 預設情況下,Eureka 上所有註冊的服務都會被 Zuul 建立對映關係來進行路由。

    #預設的規則
    zuul.routes.springcloud-consumer.path=/springcloud-consumer/**
    zuul.routes.springcloud-consumer.serviceId=springcloud-consumer
    

    但如果希望 springcloud-service-provider 作為服務提供者只對服務消費者提供服務,不對外提供服務:

    zuul.ignored-services=springcloud-service-provider
    

    還可以進一步細化,比如不想給/hello 介面路由:

    zuul.ignored-patterns=/**/hello/**
    

    也可以統一的為路由規則增加字首:

    zuul.prefix=/myapi
    

    路由規則萬用字元:

  3. 一般情況下 API 閘道器只是作為各個微服務的統一入口,但是有時候我們可能也需要在 API 閘道器服務上做一些特殊的業務邏輯處理,那麼我們可以讓請求到達 API 閘道器後,再轉發給自己本身,由 API 閘道器自己來處理,那麼我們可以進行如下的操作:

@RestController
public class GateWayController {
  @RequestMapping("/api/local")
  public String hello() {
  	return "exec the api gateway.";
  } 
}

在 application.yml 中:

zuul:
  routes:
    gateway:
      path: /gateway/**
      url: forward:/api/local

Zuul 的異常處理

首先看一下Zuul 請求的生命週期:

  • 正常情況下所有的請求都是按照 pre、route、post 的順序來執行,然後由 post 返回 response
  • 在 pre 階段,如果有自定義的過濾器則執行自定義的過濾器
  • pre、routing、post 的任意一個階段如果拋異常了,則執行 error 過濾器
    有兩種方式統一處理異常:
  1. 禁用 zuul 預設的異常處理 SendErrorFilter 過濾器,然後自定義我們自己的 Errorfilter 過濾器

    zuul:
      routes:
        springcloud-consumer: /api-zuul/**
      SendErrorFilter:
        error:
          disable: true
    
    @Component
    public class ErrorFilter extends ZuulFilter {
        private static final Logger logger =
                LoggerFactory.getLogger(ErrorFilter.class);
        @Override
        public String filterType() {
            return "error";
        }
        @Override
        public int filterOrder() {
            return 1;
        }
        @Override
        public boolean shouldFilter() {
            return true;
        }
        @Override
        public Object run() throws ZuulException {
            try {
                RequestContext context = RequestContext.getCurrentContext();
                ZuulException exception = (ZuulException)context.getThrowable();
                logger.error("進入系統異常攔截", exception);
                HttpServletResponse response = context.getResponse();
                response.setContentType("application/json; charset=utf8");
                response.setStatus(exception.nStatusCode);
                PrintWriter writer = null;
                try {
                    writer = response.getWriter();
                    writer.print("{code:"+ exception.nStatusCode +",message:\""+
                            exception.getMessage() +"\"}");
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    if(writer!=null){
                        writer.close();
                    }
                }
            } catch (Exception e) {
                ReflectionUtils.rethrowRuntimeException(e);
            }
            return null;
        }
    }
    

    在 AuthFiler 裡的run()方法新增異常 int i = 10 / 0

  2. 自定義全域性 error 錯誤頁面

    開啟 zuul 預設的異常處理 SendErrorFilter 過濾器,並註釋掉 ErrorFilter 類

@RestController
public class ErrorHandlerController implements ErrorController {
    /**
     * 出異常後進入該方法,交由下面的方法處理
     */
    @Override
    public String getErrorPath() {
        return "/error";
    }
    @RequestMapping("/error")
    public Object error(){
        RequestContext ctx       = RequestContext.getCurrentContext();
        ZuulException  exception = (ZuulException)ctx.getThrowable();
        return exception.nStatusCode + "--" + exception.getMessage();
    } 
}

相關文章