Spring Cloud系列(二十四) 路由詳解(Finchley.RC2版本)
傳統路由配置
傳統路由配置就是不需要依賴服務發現機制,通過在配置檔案中具體指定每個路由表示式與服務例項的對映關係來實現API閘道器對外請求的路由。
單例項配置
通過zuul.routes.<route>.path與zuul.routes.<route>.url的方式進行配置,比如:
zuul.routes.api-a.path= /api-a/**
zuul.routes.api-a.url= http://localhost:2222/
該配置將符合/api-a/**規則的請求路徑轉發到http://localhost:2222/地址的路由規則。比如一個請求http://localhost:5111/api-a/hello
多例項配置
通過zuul.routes.<route>.path與zuul.routes.<route>.serviceId的方式進行配置,比如:
zuul.routes.api-a.path= /api-a/** zuul.routes.api-a.serviceId= hello-service ribbon.eureka.enabled=false hello-service.ribbon.listOfServers=http://localhost:2222/,http://localhost:2223/
該配置將符合/api-a/**規則的請求路徑轉發到http://localhost:2222/和http://localhost:2223/地址的路由規則。它的配置方式和下面介紹的服務路由的配置方式一樣都要指定zuul.routes.<route>.path和zuul.routes.<route>.serviceId引數對的對映方式,只是這裡的serviceId是使用者手工命名的服務名稱,並配合ribbon.listOfServers引數實現服務於例項的維護。由於存在多個例項,API閘道器再進行路由轉發時需要實現負載均衡策略,也是這裡還需要Spring Cloud Ribbon的配合。由於在Spring Cloud Zuul已經自帶了對Ribbon的依賴,所以我們只需要做一些配置就可以了。比如上面示例中的關於Ribbon的配置:
- ribbon.eureka.enabled:由於zuul.routes.<route>.serviceId指定的是服務名稱,Ribbon預設會根據服務發現機制來獲取配置服務名對應的例項清單。但是該示例沒有整合類似Eureka之類的服務框架,所以需要將該引數設定為false,否則配置的serviceId獲取不到對應例項的清單。
- hello-service.ribbon.listOfServers:該引數內容與zuul.routes.<route>.serviceId的配置相對應,開頭的hello-servce對應了serviceId的值,這兩個引數的配置相當於在該應用內部手工維護了服務與例項的對應關係。
總結:不論是單例項還是多例項的配置方式,都需要為每一對對映關係指定一個名稱,也就是<route>,每個<route>對應了一條路由規則,每條路由規則都需要通過path屬性來定義一個用來匹配客戶端請求的路徑表示式,並通過url或者serviceId屬性來指定請求表示式對映具體例項地址或服務名。
服務路由配置
zuul.routes.api-a.path= /api-a/**
zuul.routes.api-a.serviceId= hello-service
由於Zuul整合了Eureka,所以不需要像傳統路由配置方式那樣手動指定服務例項地址。上面的示例將符合/api-a/**規則的請求路徑轉發到hello-service的服務例項上的路由規則。它還有一種更簡潔的寫法:zuul.routes.<serviceId>=<path>,其中<serviceId>用來指定具體的服務名稱,<path>用來配置匹配的請求表示式。
zuul.routes.hello-service= /api-a/**
注:如果出現ReadTimeOut異常,可以嘗試配置Ribbon的超時時間
hello-service.ribbon.ConnectTimeout=2000
hello-service.ribbon.ReadTimeout=2000
服務路由的預設規則
通過Eureka和Zuul的整合我們省去了維護例項清單的大量配置工作,剩下的只需要再維護請求路徑的匹配表示式與服務名對映關係即可。但是在實際的運用過程中會發現,大部分的路由配置規則幾乎都會採用服務名作為外部請求的字首,比如下面的例子,其中path路徑的字首使用了hello-service,而對應的服務名稱也是hello-service。
zuul.routes.hello-service.path= /hello-service/**
zuul.routes.hello-service.serviceId= hello-service
對於這樣具有規則性的配置內容,在Zuul和Eureka整合之後,它為Eureka中的每一個服務都自動建立一個預設路由規則,這些預設規則的path會使用serviceId配置的服務名作為請求字首,就和上面的例子一樣。
預設情況下,Zuul會為所有Eureka服務自動的建立對映關係來進行路由,這會使不希望對外開放的服務也可以被外部訪問到,這時候可以使用zuul.ignored-services引數來設定一個服務名匹配表示式來定義不自動建立路由的規則。Zuul在自動建立服務路由時會根據該表示式判斷,如果服務名匹配則跳過不建立預設路由規則。比如設定zuul.ignored-services=*則表示所有服務不自動建立預設路由規則,設定zuul.ignored-services=hello-service(多個逗號隔開)則表示hello-service服務不自動建立預設路由規則。
自定義路由對映規則
我們在構建微服務系統進行業務邏輯開發的時候,為了相容外部不同版本的客戶端程式(儘量不強迫使用者升級客戶端),一般會採用開閉原則來進行設計開發。這使得系統在迭代過程中,有時候會需要我們為一組互相配合的微服務定義一個版本標識來方便管理它們的版本關係,根據這個標識我們可以很容易知道哪些服務需要一起配合使用。
比如可以採用類似這樣的命名:userservice-v1、userservice-v2、orderservice-v1、orderservice-v2。預設情況下,Zuul自動為服務建立的路由表示式會採用服務名做字首,比如對應上面的對映為/userservice-v1、/userservice-v2、/orderservice-v1、/orderservice-v2,但是這樣生成的表示式規則單一,不利於通過路徑規則進行管理。通常的做法是生成版本號作為路由字首的路由規則,比如:/v1/userservice、/v2/userservice。
針對這個需求,如果我們的各個微服務都遵循了類似userservice-v1這樣的命名規則,那麼我們可以使用Zuul中自定義服務於路由對映關係的功能,來實現自動化的建立類似/v1/userservice/**的路由匹配規則。只需要在API閘道器服務的應用主類中增加如下Bean的建立即可:
@Bean
public PatternServiceRouteMapper serviceRouteMapper(){
return new PatternServiceRouteMapper(
"(?<name>^.+)-(?<version>v.+$)",
"${version}/${name}");
}
PatternServiceRouteMapper 物件可以通過曾澤表示式來自定義服務路由與對映的生成關係。其中構造方法的第一個引數是用來匹配服務名稱是否符合該自定義規則的正則表示式,而第二個引數是定義根據服務名中定義的內容轉換出的路徑表示式規則。只要符合第一個引數定義規則的服務名都會優先使用該實現構建出的路徑表示式,沒有匹配的服務則按預設的路由規則對映。
注意:使用預設的路由規則就不需要在配置檔案裡配置那些對映關係了。
路徑匹配
不論是使用傳統路由的配置還是服務路由的配置都需要為每個路由規則定義匹配表示式,也就是path引數。在Zuul中,路由匹配的表示式採用了Ant風格定義,它有下面三種萬用字元。
萬用字元 | 說明 |
? | 匹配任意單個字元 |
* | 匹配任意數量的字元 |
** | 匹配任意數量的字元,支援多及目錄 |
示例:
URL路徑 | 說明 |
/hello-service/? | 它可以匹配/hello-service/之後拼接一個任意字元的路徑。比如/hello-service/a、/hello-service/b |
/hello-service/* | 它可以匹配/hello-service/之後拼接任意字元的路徑。比如/hello-service/a、/hello-service/aaa,無法匹配/hello-service/a/b。 |
/hello-service/** | 它可以匹配/hello-service/*包含的內容外,還可以匹配形如/hello-service/a/b這樣的多級目錄路徑。 |
另外,當我們使用萬用字元的時候,可能遇到一個URL路徑被多個不同的路由的表示式匹配上的情況。比如,我們一開始構建了hello-service服務,並且配置瞭如下路由規則。
zuul.routes.hello-service.path= /hello-service/**
zuul.routes.hello-service.serviceId= hello-service
隨著版本的迭代,我們對hello-service功能做了拆分,將原屬於hello-service服務的功能拆分到了另一個全新的服務hello-service-ext中,而這些拆分的外部呼叫URL路徑希望能夠符合規則/hello-service/ext/**,這個時候我們新加路由規則
zuul.routes.hello-service-ext.path= /hello-service/ext/**
zuul.routes.hello-service-ext.serviceId= hello-service-ext
此時呼叫hello-service-ext服務的URL路徑會被 /hello-service/**和 /hello-service/ext/**所匹配。所以我們需要優先選擇/hello-service/ext/**路由,然後再匹配/hello-service/**路由。在Zuul中,是通過線性遍歷的方式,來判定請求路徑與配置檔案內配置的路由規則是否匹配,匹配就結束匹配過程,所以當存在多個匹配的路由規則時,匹配結果取決於路由規則的儲存順序。使用properties檔案無法保證有序,所以要使用yml檔案來配置,實現有序的路由規則。比如:
zuul:
routes:
hello-service-ext:
path: /hello-service/ext/**
serviceId: hello-service-ext
hello-service:
path: /hello-service/**
serviceId: hello-service
忽略表示式
通過zuul.ignored-patterns引數可以用來設定不希望被API閘道器進行路由的URL表示式。不如如果不希望/hello介面被路由可以這麼設定:
zuul.ignored-patterns=/**/hello/**
zuul.routes.api-a.path= /api-a/**
zuul.routes.api-a.serviceId= hello-service
注意:該引數並不是對某個路由設定的,而是對所有路由有效。
路由字首
Zuul提供了zuul.prefix引數來為全域性的路由規則增加字首資訊。
比如希望為閘道器上的路由規則都增加/api字首,那麼我們可以在配置檔案中增加配置:zuul.prefix=/api。另外關於代理字首會預設從路徑中移除,可以通過設定zuul.stripPrefix=false來關閉移除代理字首的動作,也可以通過zuul.routes.<route>.strip-prefix=true來指定路由關閉移除代理字首的動作。
zuul:
prefix: /api
routes:
api-a:
path: /api-a/**
serviceId: hello-service
注意:此引數在Brixton版本和Camden版本中,如果配置的字首和與路由的起始字串相同則會有Bug,比如給/api-a/配置/api字首就會有bug,這個bug在Finchley版本已經被修改。
本地跳轉
Zuul支援以forward形式的服務跳轉配置。
zuul.routes.api-a.path= /api-a/**
zuul.routes.api-a.url= http://localhost:2222/
zuul.routes.api-b.path= /api-b/**
zuul.routes.api-b.url= forward:/local
上面的示例中,會將符合/api-b/**的請求轉發到API閘道器中以/local為字首的請求上。比如http://localhost:5551/api-b/hello會被轉發到http://localhost:5551/local/hello進行本地處理,但是你必須在本地提供/local/hello的介面,如下,否則會報404錯誤。
@RestController
public class HelloController {
/**
* 本地方法
* @return
*/
@GetMapping("/local/hello")
public String hello() {
return "local";
}
}
Cookie與頭資訊
預設情況下,Spring Cloud Zuul在請求路由時,會過濾掉HTTP請求頭資訊中的一些敏感資訊,防止它們被傳遞到下游的外部伺服器。預設的敏感頭資訊通過zuul.sensitive-headers引數定義,包括Cookie、Set-Cookie、Authorization三個屬性。在Web開發時Cookie在Spring Cloud Zuul閘道器中預設不傳遞,當你使用了Shiro、Spring Security等安全框架構建的Web應用由於Cookie無法傳遞會導致無法登入和授權。解決的辦法有:
設定全域性引數為空來覆蓋預設值
zuul.sensitive-headers=
這種方法不推薦使用,這樣就破壞了預設設定的用意。
指定路由的引數來配置,方法有兩種,推薦使用。
# 方法一,對指定路由開啟自定義敏感頭
zuul.routes.<route>.customSensitiveHeaders=true
#方法二,對指定路由的敏感頭設定為空
zuul.routes.<route>.sensitiveHeaders=
例子:
zuul:
routes:
api-a:
path: /api-a/**
serviceId: hello-service
sensitive-headers:
custom-sensitive-headers: true
重定向問題
使用Shiro、Spring Security登入成功後,跳轉的頁面URL是具體的Web應用例項的地址,而不是通過閘道器的路由地址。因為使用API閘道器就是想把閘道器當作統一入口,從而不暴露所有的內部細節,所以這個問題很嚴重。導致這個問題的原因是,使用Shiro、Spring Security登入成功後通過重定向的方式跳轉到登入後的頁面,此時的請求結果狀態碼為302,請求頭資訊中的Location指向了具體的服務例項地址,而請求頭資訊中的Host也指向了具體的服務例項IP地址和埠。所以問題的根本原因在於Spring Cloud Zuul在路由請求時,Host資訊設定的不正確。解決辦法:
zuul.add-host-header=true