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