1.服務網關在微服務中的應用
(1)對外提供服務的難題分析:
微服務架構下的應用系統體系很龐大,光是需要獨立部署的基礎組件就有注冊中心、配置中心和服務總線、Turbine異常聚合和監控大盤、調用鏈追蹤器和鏈路聚合,還有Kaka和MQ之類的中間件,再加上拆分后的零散微服務模塊。—個小系統都能輕松弄出20個左右的部署包。
如果采用localhost加端口的方式直接訪問,如果這些服務—并都要提供給外部用戶訪問那該怎么辦呢?可以讓前端程序員加班加點在各個頁面給各種不同請求配置URL和端口號,人不是問題,項目完成就行。也可以我們配一個URL,通過F5或者Nginx可以做路由,話是沒錯,可是這樣就要讓運維團隊手工維護路由規則表,當我們新增刪除節點或者因為更換機房導致IP變化的時候就很麻煩。因此我們需要引入—套機制來降低路由表的維護成本。
還有一個問題就是安全性,我們在提供外部服務的時候往往會加入一些訪問控制,比如說下單接口不允許未登錄用戶的訪問,有的服務還會通過一些JWT簽名等防止客戶端篡改數據。如果讓每個服務提供者都實現同樣的訪問驗證邏輯未免有些太繁瑣,這樣純屬是增加研發人員的怒氣值,況且如果有一天我們需要更換權限認證方案,比如更換為OAuth2.0,難不成還要每個服務提供者都做變更?
我們如何對外提供服務,既能管好路由規則,還能做好訪問控制呢?在這個背景下,API網關應運而生,接待所有來訪請求。
(2)網關層
微服務引入一層專事專辦的中間層。
兩件事:
1.訪問控制,看你是否有權限訪問,拒絕未授權的來訪者
2.引導指路 問清楚你要辦的事,指一條明路。找到對應處理這些事的人
網關層引入后,微服務的架構變成:
網關層作為唯一的對外服務,外部請求不直接訪問服務層。由網關層承接所有HTTP
請求,在實際應用中。我們會將Gateway與Nginx一同使用。
(3)訪問控制和路由規則
訪問控制:
主要包含兩個方面的任務,具體的實現并不是由網關層提供的,但是網關作為一個載體承載了兩個任務:
攔截請求:有的接口需要登錄用戶才能訪問,對于這類接口的訪問,網關層可以檢查訪問請求中是否攜帶令牌等身份信息,比兔HTTP Header中的Authorization或者token屬性。如果沒有攜帶令牌,說明沒登錄,可以直接返回403 Forbidden
鑒權:對于攜帶令牌的服務,我們需要驗證令牌的真假,否則用戶可以
通過偽造的令牌進行通信,對令牌校驗失敗的請求,或者令牌已經過期
的請求執行拒絕服務!
路由規則
路由規則包含兩個方面,分別是URL映射和服務尋址
URL映射:在大多數情況下,客戶端訪問的HTTP URL往往不是我們在Controller里配置的真實路徑,比如客戶端可以發起請求"/password/update"來修改密碼,但后臺并沒有這個服務,這時候就需要網關層做一個路由規則,將來訪URL映射成真正的服務路徑,比如將剛才的密碼修改請求的路徑映射到"/user/settings/security/password"請求
服務尋址URL映射:好了之后,網關層就需要找到可以提供服務的服務器地址,對于服務集群的話,還需要實現負載均衡策略。(在Spring Cloud中,Gateway是借助Eureka的服務發現機制來實現服務尋址的,負載均衡則依靠Ribbon)
2.第二代網關組件Gateway介紹
Gateway業務場景:
3.Gateway體系架構解析
(打開Gateway的自動裝配工廠,gatewayAutoConfiguration看,第一個就是Netty)
Netty是什么?在網絡傳輸領域Netty就是身份的象征,它是非阻塞、高性能、高可靠的異步輸入輸出框架,用一個字概括就是"快"。這里我們不對Netty做深入探討,但是需要了解下Netty在Gateway中主要應用在以下幾個地方:
發起服務調用:由NettyRoutingFilter過濾器實現,底層采用基于Netty的HttpClient發起外部服務的調用
Response傳輸:由NettyResponseFilter過濾器實現,網絡請求結束后要將Response回傳給調用者
- Socket連接:具體由ReactortNettyWebSocketClient類承接,通過Netty的Httpclient發起連接請求
在Gateway中發起Request和回傳Response之類的步驟都是通過一系列過濾翮完成的,有關過濾器的內容將在稍后介紹。
(正常HTTP調用與netty的http調用的區別:
核心區別:javax.servlet.http.HttpServletRequest主要用于服務器端的HTTP請求處理,而Netty的HttpClient用于客戶端發起HTTP請求)
Client發起請求到服務網關之后,由NettyRoutingFilter底層的HttpClient(Netty組件)向
服務發起調用,調用結束后,Response有NettyResponseFilter再回傳給客戶端。有了netty加持,網絡請求效率大幅提升!Netty貫穿從Request發起到Response結束的過程,承擔了所有網絡調用相關的任務
(1)Gateway自動裝配:
AutoConfig: 作為核心自動裝配主類,GatewayAutoConfiguration負責初始化所有的Route路由規則、Predicate斷言工廠和Filter(包括Global Filter和Route Filter),這三樣是Gateway吃飯的家伙,用來完成路由功能。AutoConfig也會同時加載Netty配置
LoadBalancerGlient: 這部分在AutoConfig完成之后由GatewayLoadBalancerClientAutoConfiguration負責加載,用來加載Ribbon和一系列負載均衡配置
ClassPathWarning:同樣也是在AutoConfig完成之后觸發(具體加載類為GatewayClassPathWarningAutoConfiguration),由于Gateway底層依賴Spring WebFlux的實現,所以它會檢查項目是否加載了正確配置
Redis:在Gateway中Redis主要負責限流的功能。
除了上面幾個核心裝配工廠以外,還有兩個打醬油的路人,它們并不直接參與Gateway的核心功能,但是會提供—些重要的支持功能:
GatewayMetricsAutoConfiguration:負責做一些統計工作,比如對所謂的“short task"運行時長和調用次數做統計
GatewayDiscoveryClientAutoConfiguration:服務發現客戶端自動裝配類
爬坑指南
Gateway項目啟動出錯,但是查來查去,發現也沒有什么配置問題。這時候就要看一下是不是引入了錯誤的依賴,Gateway比較坑的一個地方是它基于WebFlux實現,因此它需要的依賴是spring-boot-starter-webflux,假如我們不小心引入了spring-boot-starter-web將導致啟動問題,
由于我們大部分的Spring Cloud項目都依賴spring-boot-starter-web,所以很容易就誤將其依賴導入到了Gateway項目中,碰到這種問題只要打印出依賴樹,排查下錯誤依賴的來源,然后將它在pom中排除出去就好了。
路由流程
這里就涉及到了Gateway最核心的路由功能,路由主要由斷言和過濾器配合來實現,我們把這部分內容拆分為3個小節,分別介紹路由的整體功能、斷言的使用、過濾器原理和生命周期。
4.路由功能詳解
Gateway網關的路由功能可不簡簡單單的轉發請求,在請求到達網關再流轉到指定服務之間發生了很多事!它不光可以拒絕請求,甚至可以篡改請求的參數!
一個route包含完整轉發規則的路由,主要由一下三部分組成:
斷言集合:斷言是路由處理的第一個環節,它是路由的匹配規則,它決定了一個網絡請求是否可以匹配給當前路由來處理。之所以它是一個集合的原因是我們可以給一個路由添加多個斷言,當每個斷言都匹配成功以后才算過了路由的第一關。有關斷言的詳細內容將在下一小節進行介紹
過濾器集合:如果請求通過了前面的斷言匹配,那就表示它被當前路由正式接手了,接下來這個請求就要經過一系列的過濾器集合。過濾器的功能就是八仙過海各顯神通了,可以對當前請求做一系列的操作,比如說權限驗證,或者將其他非業務性校驗的規則提到網關過濾器這一層。在過濾器這一層依然可以通過修改Response里的status Code達到中斷效果,比如對鑒權失敗的訪問請求設置Status Code為403之后中斷操作。有關過濾器的詳細內容將在后面的小節介紹
URI:如果請求順利通過過濾器的處理,接下來就到了最后一步,那就是轉發請求。URI是統一資源標識符,它可以是一個具體的網址,也可以是IP+端口的組合,或者是Eureka中注冊的服務名稱
關于負載均衡:
對最后一步尋址來說,如果采用基于Eureka的服務發現機制,那么在Gateway的轉發過程中可以采用服務注冊名的方式來調用,后臺會借助Ribbon實現負載均衡(可以為某個服務指定具體的負載均衡策略),其配置方式如:1b://FEIGN-SERVICE-PROVIDER/。前面的lb就是指代Ribbon作為LoadBalancer,
路由的規則流程:
Predicate Handler:具體承接類是RoutePredicateHandlerMapping。首先它獲取所有的路由(配置的routes全集),然后依次循環每個Route,把應用請求與Route中配置的所有斷言進行匹配,如果當前Route所有斷言都驗證通過,Predict Handler就選定當前的路由。這個模式是典型的職責鏈。
Filter Handler:在前一步選中路由后,由FilteringWebHandler將請求交給過濾器,在具體處理過程中,不僅當前Route中定義的過濾器會生效,我們在項目中添加的全局過濾器(Global Filter)也會一同參與。同學們看到圖中有Pre Filter和Post Filter,這是指過濾器的作用階段,我們在稍后的章節中再深入了解
尋址:這一步將把請求轉發到URI指定的地址,在發送請求之前,所有Pre類型過濾器都將被執行,而Post過濾器會在調用請求返回之后起作用。
5.斷言功能詳解(Predict)
Predicate機制:
Predicate是Java 8中引入的一個新功能,就和我們平時在項目中寫單元測試時用到的Assertion差不多,Predicate接收一個判斷條件,返回一個ture或false的布爾值結果,告知調用方判斷結果。你也可以通過and (與),or(或)和negative (非)三個操作符將多個Predicate串聯在一塊共同判斷。
如果Gateway是擋在微服務前面的中介,那這個Predicate就是和中介的接頭暗號。比如中介可以要求你的Request中必須帶有某個指定的參數叫name,對應的值必須是一個指定的信息,如果你的Request中沒有包含指定信息,或者指定信息錯誤,那就是斷言失敗。只有當你的請求完全和接頭暗號匹配的時候,中介才能給你放行。
說白了predicate就是一種路由規則,通過gateway中豐富內置斷言的組合,
我們就能讓一個請求找到對應的route來處理。
斷言的作用階段:
在一個請求抵達網關層后,首先就要進行斷言匹配,在滿足所有斷言之后
才會進入Filter階段!
常用斷言介紹:gateway提供了十多種內置斷言:
路徑匹配:path斷言是最常用一個斷言。
.route(r -> r.path(“/gateway/**”)
.uri(“lb://FEIGN-SERVICE-PROVIDER/”)
)
.route(r -> r.path(“/baidu”)
.uri(“http://baidu.com:80/”)
)
Path斷言的使用非常簡單,就像我們在Controller中配置@RequestPath的方式一樣,在Path斷言中填上—段URL匹配規則,當實際請求的URL和斷言中的規則相匹配的時候,就下發到該路由中URI指定的地址,這個地址可以是一個具體的HTTP地址,也可以是Eureka中注冊的服務名稱。在上面的例子中,如果我們訪問"Igateway/test”,這個路徑將匹配到第一個路由。
Method斷言:
這個斷言是專門驗證HTTP Method的,在下面的例子中,我們把Method斷言和Path斷言通過一個and連接符合并起來,共同作用于路由判斷,當我們訪問"lgateway/sample"并且HTTP Method是GET的時候,將適配下面的路由
.route(r -> r.path(“/gateway/“)
.and().method(HttpMethod.GET)
.uri(“lb://FEIGN-SERVICE-PROVIDER/”)
)
RequestParam匹配:請求斷言也是業務中經常使用的,它會從ServerHttpRequest中
的Parameters列表中查詢指定的屬性,如下有兩種不同的使用方式:
.route(r -> r.path(”/gateway/”)
.and().method(HttpMethod.GET)
.and().query(“name”, “test”)
.and().query(“age”)
.uri(“lb://FEIGN-SERVICE-PROVIDER/”)
)
屬性名驗證:如query(“age”),此時斷言只會驗證QueryPrameters列表中是否包含了一個叫age的屬性,并不會驗證它的值
屬性值驗證:如query ( "“name” ,“test”),它不僅會驗證name屬性是否存在,還會驗證它的值是不是和斷言相匹配,比如當前的斷言會驗證請求參數中的name屬性值是不是test,第二個參數實際上是一個用作模式匹配的正則表達式
**Header斷言:這個斷言會檢查Header中是否包含了響應的屬性,通常可以用來
驗證請求是否攜帶了令牌:
.route(r -> r.path("/gateway/")
.and().header(“Authorization”)
.uri(“lb://FEIGN-SERVICE-PROVIDER/”)
)
上面的斷言指定Header中必須包含一個Authorization屬性,Header斷言和Query斷言
一樣,也可以通過傳入兩個參數形式對屬性值進行檢查
Cookie斷言:
顧名思義,Cookie驗證的是Cookie中保存的信息,Cookie斷言和上面介紹的兩種斷言使用方式大同小異,唯一的不同是它必須連同屬性值一同驗證,不能單獨只驗證屬性是否存在,示例如下:
.route(r -> r.path(“/gateway/**”)
.and().cookie(“name”, “test”)
.uri(“lb://FEIGN-SERVICE-PROVIDER/”)
)
時間片匹配:
時間匹配有三種模式,分別是Before、After和Between,這些斷言指定了在什么時間范圍內路由才會生效
.route(r -> r.path(“/gateway/**”)
.and().before(ZonedDateTime.now().plusMinutes(1))
.uri(“lb://FEIGN-SERVICE-PROVIDER/”)
)
自定義斷言:
Gateway也提供了一個擴展方法,用來將自定義的斷言應用到路由上。老師給出兩點提示,希望同學們順著這個方向來參考Gateway的源碼,實現一個自定義斷言,完成一個小功能:將所有請求參數大于5個的訪問請求攔截掉,即RequestParam個數小于5個的請求才能被放行。
提示1:所有斷言類都可以繼承自AbstractRoutePredicateFactory
提示2:在路由配置時可以通過predicate或者asyncPredicate傳入一個自定義斷言
6.過濾器原理和生命周期
過濾器的工作模式:
Gateway的過濾器是一樣的模型,他們經過優先級排序,所有網關調用請求從最高優先級的過濾器開始,一路走到頭,直到被最后一個過濾器處理。
過濾器的實現方式:
在Gateway實現一個過濾器非常簡單,只要實現GatewayFilter接口的默認方法就好!
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 隨意發揮return chain.filter(exchange);
}
這里面有兩個關鍵信息:
ServerWebExchange:這是Spring封裝的HTTP request-response交互協議,從中我們可以獲取request和response中的各種請求參數,也可以向其中添加內容
GatewayFilterChain:它是過濾器的調用鏈,在方法結束的時候我們需要將exchange對象傳入調用鏈中的下一個對象
過濾器的執行階段:
不同于springcloud中上一代網關組件Zuul里對過濾器的Pre和Post的定義,
Gateway是通過Filter中的代碼來實現類似Pre和Post的效果!
Pre和Post是指當代過濾器執行階段,Pre是在下一個過濾器之前被執行,Post
是在過濾器執行過后再執行。我們在GatewayFileter也可以同時定義Pre和Post執行邏輯!!
Pre類型:
AddResponseHeaderGatewayFilterFactory,它可以向Response中添加Header信息:
@Override
public GatewayFilter apply(NameValueConfig config) {return (exchange, chain) -> {exchange.getResponse().getHeaders().add(config.getName(), config.getValue());return chain.filter(exchange);};
}
Post類型:
SetStatusGatewayFilterFactory,它在過濾器執行完畢之后,將制定的HTTP status返回給調用方!!
return chain.filter(exchange).then(Mono.fromRunnable(() -> {// 這里是業務邏輯}));
這個過濾器的主要邏輯在then方法中,then是一個回調函數,在下級調用鏈路都完成以后再執行,因此這類過濾器可以看做是Post Filter
過濾器排座次:
在Gateway中我們可以通過實現org.springframework.core.Ordered接口,來給過濾器指定執行順序,比如下面的代碼實現了Ordered接口方法,將過濾器執行順序設置為0:
@Override
public int getOrder() {return 0;
}
Pre
類型的過濾器來說,數字越大表示優先級越高,也就越早被執行。但對于Post類型的過濾器,則是數字越小越先被執行。
過濾器示例:
Header過濾器
這個系列有很多組過濾器,AddRequestHeader和AddResponseHeader,分別向Request和Response里加入指定Header。相應的RemoveRequestHeader和RemoveResponseHeader分別做移除操作,用法也很簡單:
.filters(f -> f.addResponseHeader("who", "gateway-header"))
上面的例子會向header中添加一個who的屬性,對應的值是gateway-heade
StringPrefix過濾器:
這是個比較常用的過濾器,它的作用是去掉部分URL路徑。比如我們的過濾器配置如下:
.route(r -> r.path("/gateway-test/**").filters(f -> f.stripPrefix(1)).uri("lb://FEIGN-SERVICE-PROVIDER/")
)
假如HTTP請求訪問的是/gateway-test/sample/update,如果沒有StripPrefix過濾器,那么轉發到FEIGN-SERVICE-PROVIDER服務的訪問路徑也是一樣的。當我們添加了這個過濾器之后,Gateway就會根據“stripPrefix(1)”中的值截取URL中的路徑,比如這里我們設置的是1,那么就去掉一個前綴,最終發送給后臺服務的路徑變成了“/sample/update”
PrefixPath過濾器:
它和StripPrefix的作用是完全相反的,會在請求路徑的前面加入前綴
.route(r -> r.path("/gateway-test/**").filters(f -> f.prefixPath("go")).uri("lb://FEIGN-SERVICE-PROVIDER/")
)
比如說我們訪問“/gateway-test/sample”的時候,上面例子中配置的過濾器就會把請求發送到“/go/gateway-test/sample”。
RedirectTo過濾器:
可以把收到特定的狀態碼的請求重定向到一個特定的網址:
.filters(f -> f.redirect(302, “https://www.xxx.com/”))
上面的例子接收HTTP status code和URL兩個參數,如果請求結果是404,則重定向到第二個參數指定的頁面,這個功能也可以做統一異常處理,將Unauthorized或Forbidden請求重定向到登錄頁面。