一、概述
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。