一、概述
API 閘道器是一個更為智慧的應用伺服器,它的定義類似於物件導向設計模式中的 Facade 模式,它的存在就像是整個微服務架構系統的門面一樣,所有的外部客戶端訪問都需要經過它來進行排程和過濾。它除了要實現請求路由、負載均衡、校驗過濾等功能之外,還需要更多能力,比如與服務治理框架的結合、請求轉發時的熔斷機制、服務的聚合等一系列高階功能。
在 Spring Cloud 中了提供了基於 Netflix Zuul 實現的 API 閘道器元件 Spring Cloud Zuul。
二、準備階段
SpringBoot 版本號:2.1.6.RELEASE
SpringCloud 版本號:Greenwich.RELEASE
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
application.yml
server:
port: 5555
spring:
application:
name: cloud-zuul
eureka:
client:
service-url:
defaultZone: http://user:password@localhost:1111/eureka/
ZuulApplication.java
// 開啟 Zuul 的Api 閘道器服務功能
@EnableZuulProxy
@EnableDiscoveryClient
@SpringBootApplication
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class, args);
}
}
三、請求轉發
Spring Cloud Zuul 通過與 Spring Cloud Eureka 進行整合,將自身註冊為 Eureka 服務治理下的應用,同時從 Eureka 中獲得了所有其他微服務的例項資訊。這樣的設計非常巧妙地將服務治理體系中維護的例項資訊利用起來,使得將維護服務例項的工作交給了服務治理框架自動完成,不再需要人工介入。而對於路由規則的維護, Zuul 預設會將通過以服務名作為
ContextPath 的方式來建立路由對映。比如上面的配置,Spring Cloud Zuul 會為 Eureka 中的每個服務都自動建立一個預設路由規則,預設規則的 path 會使用 serviceId 配置的服務名作為請求字首 —— 對於 /'serviceId'/** 的請求,會被轉發到 serviceId 的服務處理。
可以設定不對每個服務自動建立路由規則嗎?
zuul:
# Zuul 將對所有的服務都不自動建立路由規則
ignored-services: "*"
如果我們手動配置路由是怎樣的呢?推薦下面的方式:
zuul:
routes:
client-2:
path: /client-2/**
serviceId: cloud-eureka-client
# zuul.routes.<serviceid> = <path>
cloud-eureka-client: /client-3/**
client-4:
path: /client-4/**
# 請求轉發 —— 僅限轉發到本地介面
url: forward:/local
其中, ?:匹配任意單個數量字元;*:匹配任意多個數量字元;**:匹配任意多個數量字元,支援多級目錄。
不推薦使用 url 的方式來配置路由,該請求是直接通過 httpClient 包實現的, 而沒有使用 Hystrix 命令進行包裝, 所以這類請求並沒有執行緒隔離和斷路器的保護。
如果我們要過濾掉某些 url,讓它不走路由規則呢?
zuul:
# 對某些 url 設定不經過路由選擇
ignored-patterns: {"/**/world/**","/**/zuul/**"}
Spring Cloud Zuul 對 "/zuul" 的路徑訪問的會繞過 dispatcherServlet, 被 ZuulServlet 處理,主要用來應對處理大檔案上傳的情況。
zuul:
servlet-path: /zuul
四、請求過濾
Spring Cloud Zuul 提供了一套過濾器機制,開發者可以通過使用 Zuul 來建立各種校驗過濾器,然後指定哪些規則的請求需要執行校驗邏輯,只有通過校驗的才會被路由到具體的微服務介面,不然就返回錯誤提示。
要在 Zuul 實現過濾器機制也很簡單,只需要繼承 ZuulFilter 類即可。接下來,我們來寫一個過濾器 TokenFilter,校驗介面引數中是否有 token 引數。
@Component
public class TokenFilter extends ZuulFilter {
private Logger logger = LoggerFactory.getLogger(TokenFilter.class);
/**
* 過濾器的型別,它決定過濾器在請求的哪個生命週期中執行。這裡定義為 pre, 代表會在請求被路由之前執行。路由型別有下面幾種:
* <p>
* - pre: 可以在請求被路由之前呼叫。
* - routing: 在路由請求時被呼叫。
* - post: 在 routing 和 error 過濾器之後被呼叫。
* - error: 處理請求時發生錯誤時被呼叫。
*
* @return
*/
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
/**
* 過濾器的執行順序。當請求在一個階段中存在多個過濾器時,需要根據該方法返回的值來依次執行,數值越小,優先順序越高。
*
* @return
*/
@Override
public int filterOrder() {
return 0;
}
/**
* 判斷該過濾器是否需要被執行。這裡我們直接返回了true, 因此該過濾器對所有請求都會生效。實際運用中我們可以利用該函式來指定過濾器的有效範圍。
*
* @return
*/
@Override
public boolean shouldFilter() {
return true;
}
/**
* 過濾器的具體執行邏輯
*
* @return
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
logger.info("send {} request to {}", request.getMethod(), request.getRequestURL().toString());
String token = request.getParameter("token");
if (StringUtils.isEmpty(token)) {
logger.warn("token is empty");
// 令 zuul 過濾該請求,不對其進行路由
ctx.setSendZuulResponse(false);
// 設定返回的錯誤碼
ctx.setResponseStatusCode(401);
// 設定返回的 body
ctx.setResponseBody("");
return null;
}
logger.info("access token is ok");
return null;
}
}
實際上,上面提到的 Zuul 路由功能在真正執行時,它的路由對映和請求轉發都是由幾個不同的過濾器完成的。所以,過濾器可以說是 Zuul 實現 API 閘道器功能最為核心的部件,每一個進入 Zuul 的 HTTP 請求都會經過一系列的過濾器處理鏈得到請求響應並返回給客戶端。下圖源自 Zuul 的官方Wiki 中關於請求生命週期的圖解, 它描述了一個 HTTP 請求到達 API 閘道器之後, 如何在各種不同型別的過濾器之間轉的詳細過程。
當外部 HTTP 請求到達 API 閘道器服務的時候,首先它會進入第一個階段 pre, 在這裡它會被 pre 型別的過濾器進行處理, 該型別過濾器的主要目的是在進行請求路由之前做一些前置加工,比如請求的校驗、限流等。在完成了 pre 型別的過濾器處理之後,請求進入第二個階段 routing, 也就是之前說的路由請求轉發階段,請求將會被 routing 型別過濾器處理。這裡的具體處理內容就是將外部請求轉發到具體服務例項上去的過程,當服務例項將請求結果都返回之後,routing 階段完成, 請求進入第三個階段 post。此時請求將會被 post 型別的過濾器處理,這些過濾器在處理的時候不僅可以獲取到請求資訊,還能獲取到服務例項的返回資訊,所以在 post 型別的過濾器中,我們可以對處理結果進行一些加工或轉換等內容。另外,還有一個特殊的階段 error, 該階段只有在上述三個階段中發生異常的時候才會觸發,但是它的最後流向還是 post 型別的過濾器,因為它需要通過 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 | 處理正常的請求響應 |
我們可以在配置檔案中,選擇是否禁用某個過濾器。
zuul:
# 禁用某個過濾器 zuul.<SimpleClassName>.<filterTye>.disable=true
TokenFilter:
pre:
disable: true
常常 request 中有些 header 資訊我們不希望滲透到服務中去,比如 accessToken、sign、Cookie 等。或者我們要保持 request 的 host 資訊一致,該怎麼配置呢?
zuul:
routes:
client-2:
path: /client-2/**
serviceId: cloud-eureka-client
# 敏感頭資訊設定為空,表示不過濾敏感頭資訊,允許敏感頭資訊滲透到下游伺服器(針對單個服務的敏感頭部資訊配置,下面兩個配置項選其一即可)
sensitiveHeaders: ""
customSensitiveHeaders: true
# Spring Cloud Zuul在請求路由時,會過濾掉 HTTP 請求頭(Cookie、Set-Cookie、Authorization)資訊中的一些敏感資訊,
sensitive-headers: {"Cookie", "Set-Cookie", "Authorization"}
# 閘道器在進行路由轉發時為請求設定 Host 頭資訊(保持在路由轉發過程中 host 頭資訊不變)
add-host-header: true
# 請求轉發時加上 X-Forwarded-*頭域
add-proxy-headers: true
五、Hystrix 和 Ribbon 支援
# 該引數可以用來設定 API 閘道器中路由轉發請求的 HystrixCommand 執行超時時間,單位為毫秒。
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutinMilliseconds: 5000
ribbon:
# 該引數用來設定路由轉發請求的時候,建立請求連線的超時時間。
ConnectTimeout: 500
# 該引數用來設定路由轉發請求的超時時間。
ReadTimeout: 2000
# 最大自動重試次數
MaxAutoRetries: 1
# 最大自動重試下一個服務的次數
MaxAutoRetriesNextServer: 1
其中,Hystrix 的配置引數可以在 HystrixCommandProperties.java 中找到。
其中,Ribbon 的配置引數可以在 CommonClientConfigKey.java 中找到。
另外需要注意的是,請求重試還需要將 zuul.retryable 設定為 true。