服務網關ZUUL過濾器和Spring Filter執行順序詳解

原文鏈接:https://www.cnblogs.com/duanxz/p/7542150.html

以下轉發別人的文章,挺不錯的,分析的很深入,拿過來給大家分享下。

Zuul的核心是一系列的過濾器,這些過濾器可以完成以下功能:

  • 身份認證與安全:識別每個資源的驗證要求,並拒絕那些與要求不符的請求。
  • 審查與監控:在邊緣位置追蹤有意義的數據和統計結果,從而帶來精確的生成視圖。
  • 動態路由:動態地將請求路由到不同的後端集羣。
  • 壓力測試:逐漸增加執行集羣的流量,以瞭解性能。
  • 負載分配:爲每一種負載類型分配對應容量,並棄用超出限定值得請求。
  • 靜態響應處理:在邊緣位置直接建立部分響應,從而避免其轉發到內部集羣。
  • 多區域彈性:跨越AWS Region進行請求路由,旨在實現ELB(Elastic Load Balancing)使用的多樣化,以及讓系統的邊緣更貼近系統的使用者。
  • 在實現了請求路由功能後,我們的微服務應用提供的接口就可以通過統一的API網關入口被客戶端訪問到了。但是,每個客戶端用戶請求爲服務器應用提供的接口時,它們的訪問權限往往都有一定的限制,系統並不會將所有的微服務接口都對它們開放。

在完成了服務路由之後,我們對外開放服務還需要一些安全措施來保護客戶端只能訪問它應該訪問到的資源。所以我們需要利用Zuul的過濾器來實現我們對外服務的安全控制。

在服務網關中定義過濾器只需要繼承ZuulFilter抽象類實現其定義的四個抽象函數就可對請求進行攔截與過濾。

比如下面的例子,定義了一個Zuul過濾器,實現了在請求被路由之前檢查請求中是否有accessToken參數,若有就進行路由,若沒有就拒絕訪問,返回401 Unauthorized錯誤。

package com.dxz.zuul;

import javax.servlet.http.HttpServletRequest;

import org.apache.log4j.Logger;

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

public class AccessFilter extends ZuulFilter {

    private static Logger log = Logger.getLogger(AccessFilter.class);

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

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

    @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()));

        Object accessToken = request.getParameter("accessToken");
        if (accessToken == null) {
            log.warn("access token is empty");
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(401);
            return null;
        }
        log.info("access token ok");
        return null;
    }

}

在啓動類裏爲自定義過濾器創建具體的Bean才能啓動該過濾器,如下:

    @Bean
    public AccessFilter accessFilter() {
        return new AccessFilter();
    }

啓動該服務網關後,訪問:

  • http://127.0.0.1:5555/api-b/add?a=1&b=5&sn=1:返回401錯誤
  • http://127.0.0.1:5555/api-b/add?a=1&b=5&sn=1&accessToken=token:正確路由到server,並返回計算內容

不可訪問情況:

可訪問情況:

 

過濾器

過濾器兩個功能:
1、其中路由功能負責將外部請求轉發到具體的微服務實例上,是實現外部訪問統一入口的基礎;
2、過濾器功能則負責對請求的處理過程進行預干預,是實現請求校驗、服務聚合等功能的基礎。

ZuulFilter抽象類

有4類可重寫的方法來自定義過濾器,如下:

  • filterType:返回一個字符串代表過濾器的類型,在zuul中定義了四種不同生命週期的過濾器類型,具體如下:自定義過濾器的實現,需要繼承ZuulFilter,需要重寫實現下面四個方法:
    • pre:可以在請求被路由之前調用
    • routing:在路由請求時候被調用
    • post:在routing和error過濾器之後被調用
    • error:處理請求時發生錯誤時被調用
  • filterOrder:通過int值來定義過濾器的執行順序
  • shouldFilter:返回一個boolean類型來判斷該過濾器是否要執行,所以通過此函數可實現過濾器的開關。在上例中,我們直接返回true,所以該過濾器總是生效。
  • run:過濾器的具體邏輯。需要注意,這裏我們通過ctx.setSendZuulResponse(false)令zuul過濾該請求,不對其進行路由,然後通過ctx.setResponseStatusCode(401)設置了其返回的錯誤碼,當然我們也可以進一步優化我們的返回,比如,通過ctx.setResponseBody(body)對返回body內容進行編輯等。

 請求生命週期

對於其他一些過濾類型,這裏就不一一展開了,根據之前對filterType生命週期介紹,可以參考下圖去理解,並根據自己的需要在不同的生命週期中去實現不同類型的過濾器。

Zuul大部分功能都是通過過濾器來實現的,這些過濾器類型對應於請求的典型生命週期:

  • PRE: 這種過濾器在請求被路由之前調用。我們可利用這種過濾器實現身份驗證、在集羣中選擇請求的微服務、記錄調試信息等。
  • ROUTING:這種過濾器將請求路由到微服務。這種過濾器用於構建發送給微服務的請求,並使用Apache HttpClient或Netfilx Ribbon請求微服務。
  • POST:這種過濾器在路由到微服務以後執行。這種過濾器可用來爲響應添加標準的HTTP Header、收集統計信息和指標、將響應從微服務發送給客戶端等。
  • ERROR:在其他階段發生錯誤時執行該過濾器。 
    除了默認的過濾器類型,Zuul還允許我們創建自定義的過濾器類型。例如,我們可以定製一種STATIC類型的過濾器,直接在Zuul中生成響應,而不將請求轉發到後端的微服務。

在完成了pre類型的過濾器處理之後,請求進入第二個階段routing,也就是之前說的路由請求轉發階段,請求將會被routing類型過濾器處理,這裏的具體處理內容就是將外部請求轉發到具體服務實例上去的過程,當服務實例將請求結果都返回之後,routing階段完成,請求進入第三個階段post,此時請求將會被post類型的過濾器進行處理,這些過濾器在處理的時候不僅可以獲取到請求信息,還能獲取到服務實例的返回信息,所以在post類型的過濾器中,我們可以對處理結果進行一些加工或轉換等內容。

另外,還有一個特殊的階段error,該階段只有在上述三個階段中發生異常的時候纔會觸發,但是它的最後流向還是post類型的過濾器,因爲它需要通過post過濾器將最終結果返回給請求客戶端(實際實現上還有一些差別,後續介紹)。

核心過濾器

在Spring Cloud Zuul中,爲了讓API網關組件可以更方便的上手使用,它在HTTP請求生命週期的各個階段默認地實現了一批覈心過濾器,它們會在API網關服務啓動的時候被自動地加載和啓用。我們可以在源碼中查看和了解它們,它們定義於spring-cloud-netflix-core模塊的org.springframework.cloud.netflix.zuul.filters包下。

如上圖所示,在默認啓用的過濾器中包含了三種不同生命週期的過濾器,這些過濾器都非常重要,可以幫助我們理解Zuul對外部請求處理的過程,以及幫助我們如何在此基礎上擴展過濾器去完成自身系統需要的功能。下面,我們將逐個地對這些過濾器做一些詳細的介紹:

pre過濾器

  • ServletDetectionFilter:它的執行順序爲-3,是最先被執行的過濾器。該過濾器總是會被執行,主要用來檢測當前請求是通過Spring的DispatcherServlet處理運行,還是通過ZuulServlet來處理運行的。

    它的檢測結果會以布爾類型保存在當前請求上下文的isDispatcherServletRequest參數中,這樣在後續的過濾器中,我們就可以通過RequestUtils.isDispatcherServletRequest()和RequestUtils.isZuulServletRequest()方法判斷它以實現做不同的處理。

    一般情況下,發送到API網關的外部請求都會被Spring的DispatcherServlet處理,除了通過/zuul/路徑訪問的請求會繞過DispatcherServlet,被ZuulServlet處理,主要用來應對處理大文件上傳的情況。另外,對於ZuulServlet的訪問路徑/zuul/,我們可以通過zuul.servletPath參數來進行修改。

  • Servlet30WrapperFilter:它的執行順序爲-2,是第二個執行的過濾器。目前的實現會對所有請求生效,主要爲了將原始的HttpServletRequest包裝成Servlet30RequestWrapper對象。
  • FormBodyWrapperFilter:它的執行順序爲-1,是第三個執行的過濾器。

    該過濾器僅對兩種類請求生效,第一類是Content-Type爲application/x-www-form-urlencoded的請求,第二類是Content-Type爲multipart/form-data並且是由Spring的DispatcherServlet處理的請求(用到了ServletDetectionFilter的處理結果)。

    而該過濾器的主要目的是將符合要求的請求體包裝成FormBodyRequestWrapper對象。

  • DebugFilter:它的執行順序爲1,是第四個執行的過濾器。

    該過濾器會根據配置參數zuul.debug.request和請求中的debug參數來決定是否執行過濾器中的操作。而它的具體操作內容則是將當前的請求上下文中的debugRouting和debugRequest參數設置爲true。

  由於在同一個請求的不同生命週期中,都可以訪問到這兩個值,所以我們在後續的各個過濾器中可以利用這兩值來定義一些debug信息,這樣當線上環境出現問題的時候,可以通過請求參數的方式來激活這些debug信息以幫助分析問題。

    另外,對於請求參數中的debug參數,我們也可以通過zuul.debug.parameter來進行自定義。

  •   PreDecorationFilter:它的執行順序爲5,是pre階段最後被執行的過濾器。該過濾器會判斷當前請求上下文中是否存在forward.to和serviceId參數,如果都不存在,那麼它就會執行具體過濾器的操作(如果有一個存在的話,說明當前請求已經被處理過了,因爲這兩個信息就是根據當前請求的路由信息加載進來的)。

    而它的具體操作內容就是爲當前請求做一些預處理,比如:進行路由規則的匹配、在請求上下文中設置該請求的基本信息以及將路由匹配結果等一些設置信息等,這些信息將是後續過濾器進行處理的重要依據,我們可以通過RequestContext.getCurrentContext()來訪問這些  信息。

  另外,我們還可以在該實現中找到一些對HTTP頭請求進行處理的邏輯,其中包含了一些耳熟能詳的頭域,比如:X-Forwarded-Host、X-Forwarded-Port。

  另外,對於這些頭域的記錄是通過zuul.addProxyHeaders參數進行控制的,而這個參數默認值爲true,所以Zuul在請求跳轉時默認地會爲請求增加X-Forwarded-*頭域,包括:X-Forwarded-Host、X-Forwarded-Port、X-Forwarded-For、X-Forwarded-Prefix、X-Forwarded-    Proto。

  我們也可以通過設置zuul.addProxyHeaders=false關閉對這些頭域的添加動作。

route過濾器

  • RibbonRoutingFilter:它的執行順序爲10,是route階段第一個執行的過濾器。該過濾器只對請求上下文中存在serviceId參數的請求進行處理,即只對通過serviceId配置路由規則的請求生效。而該過濾器的執行邏輯就是面向服務路由的核心,它通過使用Ribbon和Hystrix來向服務實例發起請求,並將服務實例的請求結果返回。
  • SimpleHostRoutingFilter:它的執行順序爲100,是route階段第二個執行的過濾器。該過濾器只對請求上下文中存在routeHost參數的請求進行處理,即只對通過url配置路由規則的請求生效。而該過濾器的執行邏輯就是直接向routeHost參數的物理地址發起請求,從源碼中我們可以知道該請求是直接通過httpclient包實現的,而沒有使用Hystrix命令進行包裝,所以這類請求並沒有線程隔離和斷路器的保護。

  知道配置類似zuul.routes.user-service.url=http://localhost:8080/這樣的底層都是通過httpclient直接發送請求的,也就知道爲什麼這樣的情況沒有做到負載均衡的原因所在。

  • SendForwardFilter:它的執行順序爲500,是route階段第三個執行的過濾器。該過濾器只對請求上下文中存在forward.to參數的請求進行處理,即用來處理路由規則中的forward本地跳轉配置。

post過濾器

  • SendErrorFilter:它的執行順序爲0,是post階段第一個執行的過濾器。該過濾器僅在請求上下文中包含error.status_code參數(由之前執行的過濾器設置的錯誤編碼)並且還沒有被該過濾器處理過的時候執行。而該過濾器的具體邏輯就是利用請求上下文中的錯誤信息來組織成一個forward到API網關/error錯誤端點的請求來產生錯誤響應。
  • SendResponseFilter:它的執行順序爲1000,是post階段最後執行的過濾器。該過濾器會檢查請求上下文中是否包含請求響應相關的頭信息、響應數據流或是響應體,只有在包含它們其中一個的時候就會執行處理邏輯。而該過濾器的處理邏輯就是利用請求上下文的響應信息來組織需要發送回客戶端的響應內容。

下表是對上述過濾器根據順序、名稱、功能、類型做了綜合的整理,可以幫助我們在自定義過濾器或是擴展過濾器的時候用來參考並全面地考慮整個請求生命週期的處理過程。

Zuul中默認實現的Filter

類型 順序 過濾器 功能
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 處理正常的請求響應

禁用指定的Filter

可以在application.yml中配置需要禁用的filter,格式:

zuul:
    FormBodyWrapperFilter:
        pre:
            disable: true

Zuul中的Filter執行順序控制

1、定義是給出執行順序

ZuulFilter抽象類實現了Comparable接口,並實現了compareTo方法(zuul-core-1.3.0.jar的ZuulFilter.java)

 public int compareTo(ZuulFilter filter) {
        return Integer.compare(this.filterOrder(), filter.filterOrder());
    }

2、Filter執行是按照順序執行

zuul-core-1.3.0.jar的FilterProcessor.java

public Object runFilters(String sType) throws Throwable {
        if (RequestContext.getCurrentContext().debugRouting()) {
            Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
        }
        boolean bResult = false;
        List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
        if (list != null) {
            for (int i = 0; i < list.size(); i++) {
                ZuulFilter zuulFilter = list.get(i);
                Object result = processZuulFilter(zuulFilter);
                if (result != null && result instanceof Boolean) {
                    bResult |= ((Boolean) result);
                }
            }
        }
        return bResult;
    }

zuul-core-1.3.0.jar的FilterLoader.java的getFilterByType中按類型pre、route、post取Filter後,再按照FilterOrder進行排序。

public List<ZuulFilter> getFiltersByType(String filterType) {

        List<ZuulFilter> list = hashFiltersByType.get(filterType);
        if (list != null) return list;

        list = new ArrayList<ZuulFilter>();

        Collection<ZuulFilter> filters = filterRegistry.getAllFilters();
        for (Iterator<ZuulFilter> iterator = filters.iterator(); iterator.hasNext(); ) {
            ZuulFilter filter = iterator.next();
            if (filter.filterType().equals(filterType)) {
                list.add(filter);
            }
        }
        Collections.sort(list); // sort by priority

        hashFiltersByType.putIfAbsent(filterType, list);
        return list;
    }

zuul-core-1.3.0.jar的FilterProcessor.java的processZuulFilter方法執行具體Filter

public Object processZuulFilter(ZuulFilter filter) throws ZuulException {

        RequestContext ctx = RequestContext.getCurrentContext();
        boolean bDebug = ctx.debugRouting();
        final String metricPrefix = "zuul.filter-";
        long execTime = 0;
        String filterName = "";
        try {
            long ltime = System.currentTimeMillis();
            filterName = filter.getClass().getSimpleName();
            
            RequestContext copy = null;
            Object o = null;
            Throwable t = null;

            if (bDebug) {
                Debug.addRoutingDebug("Filter " + filter.filterType() + " " + filter.filterOrder() + " " + filterName);
                copy = ctx.copy();
            }
            
            ZuulFilterResult result = filter.runFilter();
            ExecutionStatus s = result.getStatus();
            execTime = System.currentTimeMillis() - ltime;

            switch (s) {
                case FAILED:
                    t = result.getException();
                    ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
                    break;
                case SUCCESS:
                    o = result.getResult();
                    ctx.addFilterExecutionSummary(filterName, ExecutionStatus.SUCCESS.name(), execTime);
                    if (bDebug) {
                        Debug.addRoutingDebug("Filter {" + filterName + " TYPE:" + filter.filterType() + " ORDER:" + filter.filterOrder() + "} Execution time = " + execTime + "ms");
                        Debug.compareContextState(filterName, copy);
                    }
                    break;
                default:
                    break;
            }
            
            if (t != null) throw t;

            usageNotifier.notify(filter, s);
            return o;

        } catch (Throwable e) {
            if (bDebug) {
                Debug.addRoutingDebug("Running Filter failed " + filterName + " type:" + filter.filterType() + " order:" + filter.filterOrder() + " " + e.getMessage());
            }
            usageNotifier.notify(filter, ExecutionStatus.FAILED);
            if (e instanceof ZuulException) {
                throw (ZuulException) e;
            } else {
                ZuulException ex = new ZuulException(e, "Filter threw Exception", 500, filter.filterType() + ":" + filterName);
                ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
                throw ex;
            }
        }
    }

再調用ZuulFilter抽象類的runFilter()方法,該方法執行子類(自定義Filter)的run方法,完成我們的業務邏輯。

public ZuulFilterResult runFilter() {
        ZuulFilterResult zr = new ZuulFilterResult();
        if (!isFilterDisabled()) {
            if (shouldFilter()) {
                Tracer t = TracerFactory.instance().startMicroTracer("ZUUL::" + this.getClass().getSimpleName());
                try {
                    Object res = run();
                    zr = new ZuulFilterResult(res, ExecutionStatus.SUCCESS);
                } catch (Throwable e) {
                    t.setName("ZUUL::" + this.getClass().getSimpleName() + " failed");
                    zr = new ZuulFilterResult(ExecutionStatus.FAILED);
                    zr.setException(e);
                } finally {
                    t.stopAndLog();
                }
            } else {
                zr = new ZuulFilterResult(ExecutionStatus.SKIPPED);
            }
        }
        return zr;
    }

參考地址https://www.cnblogs.com/duanxz/p/7542150.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章