1. 需求背景
在微服務架構中,通常一個系統會被拆分為多個微服務,面對這么多微服務客戶端應該如何去調用呢?如果根據每個微服務的地址發起調用,存在如下問題:
- 客戶端多次請求不同的微服務,會增加客戶端代碼和配置的復雜性,維護成本比價高
- 認證復雜,每個微服務可能存在不同的認證方式,客戶端去調用,要去適配不同的認證
- 存在跨域的請求,調用鏈有一定的相對復雜性(防火墻 / 瀏覽器不友好的協議)
- 難以重構,隨著項目的迭代,可能需要重新劃分微服務
為了解決上面的問題,微服務引入了API網關的概念,API網關為微服務架構的系統提供簡單、有效且統一的API路由管理,作為系統的統一入口,提供內部服務的路由中轉,給客戶端提供統一的服務,可以實現一些和業務沒有耦合的公用邏輯,主要功能包含認證、鑒權、路由轉發、安全策略、防刷、流量控制、監控日志等。
2. 什么是Spring Cloud Gateway
Spring Cloud Gateway 是Spring Cloud官方推出的第二代網關框架,定位于取代 Netflix Zuul。Spring Cloud Gateway 旨在為微服務架構提供一種簡單且有效的 API 路由的管理方式,并基于 Filter 的方式提供網關的基本功能,例如說安全認證、監控、限流等等。
Spring Cloud Gateway 是由 WebFlux + Netty + Reactor 實現的響應式的 API 網關。它不能在傳統的 servlet 容器中工作,也不能構建成 war 包。
官網文檔:Spring Cloud Gateway
2.1?核心概念
- 路由(route)
路由是網關中最基礎的部分,路由信息包括一個ID、一個目的URI、一組斷言工廠、一組Filter組成。
- 斷言(predicates)
Java8中的斷言函數,SpringCloud Gateway中的斷言函數類型是Spring5.0框架中的ServerWebExchange。斷言函數允許開發者去定義匹配Http request中的任何信息,比如請求頭和參數等。如果斷言為真,則說明請求的URL和配置的路由匹配。
- 過濾器(Filter)
SpringCloud Gateway中的filter分為Gateway FilIer和Global Filter。Filter可以對請求和響應進行處理。
2.2 工作原理
Spring Cloud Gateway 的工作原理跟 Zuul 的差不多,最大的區別就是 Gateway 的 Filter 只有 pre 和 post 兩種。
客戶端向 Spring Cloud Gateway 發出請求,如果請求與網關程序定義的路由匹配,則該請求就會被發送到網關 Web 處理程序,此時處理程序運行特定的請求過濾器鏈。
過濾器之間用虛線分開的原因是過濾器可能會在發送代理請求的前后執行邏輯。所有 pre 過濾器邏輯先執行,然后執行代理請求;代理請求完成后,執行 post 過濾器邏輯。
3. Spring Cloud Gateway實戰
3.1 微服務快速接入Spring Cloud Gateway
1) 引入依賴
<!-- gateway網關 -->
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId>
</dependency><!-- nacos服務注冊與發現 -->
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
注意:gateway會和spring-webmvc的依賴沖突,需要排除spring-webmvc
2) 編寫yml配置文件
spring:application:name: mall-gateway#配置nacos注冊中心地址cloud:nacos:discovery:server-addr: 127.0.0.1:8848gateway:#設置路由:路由id、路由到微服務的uri、斷言routes:- id: order_route #路由ID,全局唯一,建議配置服務名uri: lb://mall-order #lb 整合負載均衡器loadbalancerpredicates:- Path=/order/** # 斷言,路徑相匹配的進行路由- id: user_route #路由ID,全局唯一,建議配置服務名uri: lb://mall-user #lb 整合負載均衡器loadbalancerpredicates:- Path=/user/** # 斷言,路徑相匹配的進行路由
3)測試
http://localhost:8888/order/findOrderByUserId/1
3.2 路由斷言工廠(Route Predicate Factories)配置
predicates:路由斷言,判斷請求是否符合要求,符合則轉發到路由目的地。application.yml配置文件中寫的斷言規則只是字符串,這些字符串會被Predicate Factory讀取并處理,轉變為路由判斷的條件
文檔:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gateway-request-predicates-factories
通過網關啟動日志,可以查看內置路由斷言工廠:
3.2.1 路徑匹配
spring:cloud:gateway:#設置路由:路由id、路由到微服務的uri、斷言routes:- id: order_route #路由ID,全局唯一uri: lb://mall-order #目標微服務的請求地址和端口predicates:# 測試:http://localhost:8888/order/findOrderByUserId/1- Path=/order/** # 斷言,路徑相匹配的進行路由
3.2.2 Header匹配
spring:cloud:gateway:#設置路由:路由id、路由到微服務的uri、斷言routes:- id: order_route #路由ID,全局唯一uri: lb://mall-order #目標微服務的請求地址和端口predicates:- Path=/order/** # 斷言,路徑相匹配的進行路由# Header匹配 請求中帶有請求頭名為 x-request-id,其值與 \d+ 正則表達式匹配- Header=X-Request-Id, \d+
測試
3.3 過濾器工廠(?GatewayFilter?Factories)配置
GatewayFilter是網關中提供的一種過濾器,可以對進入網關的請求和微服務返回的響應做處理
https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gatewayfilter-factories
3.3.1 添加請求頭
需求:給所有進入mall-order的請求添加一個請求頭:X-Request-color=red。
只需要修改gateway服務的application.yml文件,添加路由過濾即可:
spring:cloud:gateway:#設置路由:路由id、路由到微服務的uri、斷言routes:- id: order_route #路由ID,全局唯一uri: http://localhost:8020 #目標微服務的請求地址和端口#配置過濾器工廠filters:- AddRequestHeader=X-Request-color, red #添加請求頭
測試http://localhost:8888/order/testgateway
@GetMapping("/testgateway")
public String testGateway(HttpServletRequest request) throws Exception {log.info("gateWay獲取請求頭X-Request-color:"+request.getHeader("X-Request-color"));return "success";
}
@GetMapping("/testgateway2")
public String testGateway(@RequestHeader("X-Request-color") String color) throws Exception {log.info("gateWay獲取請求頭X-Request-color:"+color);return "success";
}
3.3.2 添加請求參數
spring:cloud:gateway:#設置路由:路由id、路由到微服務的uri、斷言routes:- id: order_route #路由ID,全局唯一uri: http://localhost:8020 #目標微服務的請求地址和端口#配置過濾器工廠filters:- AddRequestParameter=color, blue # 添加請求參數
測試http://localhost:8888/order/testgateway3
@GetMapping("/testgateway3")
public String testGateway3(@RequestParam("color") String color) throws Exception {log.info("gateWay獲取請求參數color:"+color);return "success";
}
3.3.3 自定義過濾器工廠
繼承AbstractNameValueGatewayFilterFactory且我們的自定義名稱必須要以GatewayFilterFactory結尾并交給spring管理。
@Component
@Slf4j
public class CheckAuthGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {@Overridepublic GatewayFilter apply(NameValueConfig config) {return (exchange, chain) -> {log.info("調用CheckAuthGatewayFilterFactory==="+ config.getName() + ":" + config.getValue());return chain.filter(exchange);};}
}
配置自定義的過濾器工廠
spring:cloud:gateway:#設置路由:路由id、路由到微服務的uri、斷言routes:- id: order_route #路由ID,全局唯一uri: http://localhost:8020 #目標微服務的請求地址和端口#配置過濾器工廠filters:- CheckAuth=fox,男 #自定義過濾器工廠
測試
3.4 全局過濾器(Global Filters)配置
全局過濾器的作用也是處理一切進入網關的請求和微服務響應,與GatewayFilter的作用一樣。
- GatewayFilter:網關過濾器,需要通過spring.cloud.routes.filters配置在具體的路由下,只作用在當前特定路由上,也可以通過配置spring.cloud.default-filters讓它作用于全局路由上。
- GlobalFilter:全局過濾器,不需要再配置文件中配置,作用在所有的路由上,最終通過GatewayFilterAdapter包裝成GatewayFilterChain能夠識別的過濾器。
https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#global-filters
3.4.1 ReactiveLoadBalancerClientFilter
ReactiveLoadBalancerClientFilter 會查看exchange的屬性ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR的值(一個URI,比如lb://mall-order/order/testgateway2?color=blue),如果該值的scheme是 lb,比如:lb://myservice ,它將會使用Spring Cloud的LoadBalancerClient 來將 myservice 解析成實際的host和port。
其實就是用來整合負載均衡器loadbalancer的
spring:cloud:gateway:routes:- id: order_routeuri: lb://mall-orderpredicates:- Path=/order/**
3.4.2 自定義全局過濾器
自定義全局過濾器定義方式是實現GlobalFilter接口。每一個過濾器都必須指定一個int類型的order值,order值越小,過濾器優先級越高,執行順序越靠前。GlobalFilter通過實現Ordered接口來指定order值
@Component
@Slf4j
public class CheckAuthFilter implements GlobalFilter, Ordered {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {//獲取tokenString token = exchange.getRequest().getHeaders().getFirst("token");if (null == token) {log.info("token is null");ServerHttpResponse response = exchange.getResponse();response.getHeaders().add("Content-Type","application/json;charset=UTF-8");// 401 用戶沒有訪問權限response.setStatusCode(HttpStatus.UNAUTHORIZED);byte[] bytes = HttpStatus.UNAUTHORIZED.getReasonPhrase().getBytes();DataBuffer buffer = response.bufferFactory().wrap(bytes);// 請求結束,不繼續向下請求return response.writeWith(Mono.just(buffer));}//TODO 校驗token進行身份認證log.info("校驗token");return chain.filter(exchange);}@Overridepublic int getOrder() {return 2;}
}
3.5 Gateway跨域資源共享配置(CORS Configuration)
在前端領域中,跨域是指瀏覽器允許向服務器發送跨域請求,從而克服Ajax只能同源使用的限制。
同源策略(Same Orgin Policy)是一種約定,它是瀏覽器核心也最基本的安全功能,它會阻止一個域的js腳本和另外一個域的內容進行交互,如果缺少了同源策略,瀏覽器很容易受到XSS、CSRF等攻擊。所謂同源(即在同一個域)就是兩個頁面具有相同的協議(protocol)、主機(host)和端口號(port)。
CORS: 跨源資源共享(CORS) - HTTP | MDN
測試代碼
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title><script src="http://apps.bdimg.com/libs/jquery/1.9.1/jquery.min.js"></script>
</head>
<body><div ><table border="1"><thead><tr><th>id</th><th>userId</th><th>commodityCode</th><th>count</th><th>amount</th></tr></thead><tbody id="orderlist"></tbody></table></div><input type="button" value="訂單列表" onclick="getData()"><script>function getData() {$.get('http://localhost:8888/order/findOrderByUserId/1',function(data){console.log(data.orders)var list = data.orders;var str = '';for(var i=0;i<list.length;i++){str +='<tr><th>'+list[i].id+'</th><th>'+list[i].userId+'</th><th>'+list[i].commodityCode+'</th><th>'+list[i].count+'</th><th>'+list[i].amount+'</th></tr>';}$('#orderlist').html(str);});}</script>
</body>
</html>
測試結果
如何解決gateway跨域問題?
通過yml配置的方式
https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#cors-configuration
spring:cloud:gateway:globalcors:cors-configurations:'[/**]':allowedOrigins: "*"allowedMethods:- GET- POST- DELETE- PUT- OPTION
通過java配置的方式
@Configuration
public class CorsConfig {@Beanpublic CorsWebFilter corsFilter() {CorsConfiguration config = new CorsConfiguration();config.addAllowedMethod("*");config.addAllowedOrigin("*");config.addAllowedHeader("*");UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());source.registerCorsConfiguration("/**", config);return new CorsWebFilter(source);}
}
3.6 Gateway基于redis+lua腳本限流
spring cloud官方提供了RequestRateLimiter過濾器工廠,基于redis+lua腳本方式采用令牌桶算法實現了限流。
https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#the-requestratelimiter-gatewayfilter-factory
請求不被允許時返回狀態:HTTP 429 - Too Many Requests。
1)添加依賴
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId>
</dependency>
2)修改?application.yml?,添加redis配置和RequestRateLimiter過濾器工廠配置
spring:application:name: mall-gatewaydata:#配置redis地址redis:host: localhostport: 6379database: 0timeout: 5000lettuce:pool:max-active: 200max-wait: 10000max-idle: 100min-idle: 10#配置nacos注冊中心地址cloud:nacos:discovery:server-addr: 127.0.0.1:8848gateway:#設置路由:路由id、路由到微服務的uri、斷言routes:- id: order_route #路由ID,全局唯一,建議配置服務名# 測試 http://localhost:8888/order/findOrderByUserId/1uri: lb://mall-order #lb 整合負載均衡器ribbon,loadbalancerpredicates:- Path=/order/** # 斷言,路徑相匹配的進行路由#配置過濾器工廠filters:- name: RequestRateLimiter #限流過濾器args:redis-rate-limiter.replenishRate: 1 #令牌桶每秒填充速率redis-rate-limiter.burstCapacity: 2 #令牌桶的總容量key-resolver: "#{@keyResolver}" #使用SpEL表達式,從Spring容器中獲取Bean對象
3) 配置keyResolver,可以指定限流策略,比如url限流,參數限流,ip限流等等
@Bean
KeyResolver keyResolver() {//url限流return exchange -> Mono.just(exchange.getRequest().getURI().getPath());//參數限流//return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
}
4) 測試
url限流:http://localhost:8888/order/findOrderByUserId/1?
參數限流:http://localhost:8888/order/findOrderByUserId/1?user=fox
3.7 Gateway整合sentinel限流
從 1.6.0 版本開始,Sentinel 提供了 Spring Cloud Gateway 的適配模塊,可以提供兩種資源維度的限流:
- route 維度:即在 Spring 配置文件中配置的路由條目,資源名為對應的 routeId
- 自定義 API 維度:用戶可以利用 Sentinel 提供的 API 來自定義一些 API 分組
sentinel網關流控:api-gateway-flow-control | Sentinel
3.7.1 Gateway整合sentinel實現網關限流
1)引入依賴
<!-- gateway接入sentinel -->
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
2)添加yml配置,接入sentinel dashboard,通過sentinel控制臺配置網關流控規則
server:port: 8888
spring:application:name: mall-gateway-sentinel-demomain:allow-bean-definition-overriding: true#配置nacos注冊中心地址cloud:nacos:discovery:server-addr: 127.0.0.1:8848sentinel:transport:# 添加sentinel的控制臺地址dashboard: 127.0.0.1:8080gateway:#設置路由:路由id、路由到微服務的uri、斷言routes:- id: order_route #路由ID,全局唯一,建議配合服務名uri: lb://mall-order #lb 整合負載均衡器loadbalancerpredicates:- Path=/order/**- id: user_routeuri: lb://mall-user #lb 整合負載均衡器loadbalancerpredicates:- Path=/user/**
注意:基于SpringBoot3的 Spring Cloud Gateway和Sentinel還存在兼容性問題,等待Sentinel官方對最新的Gateway適配包更新
3.7.2 Sentinel網關流控實現原理
當通過?GatewayRuleManager?加載網關流控規則(GatewayFlowRule)時,無論是否針對請求屬性進行限流,Sentinel 底層都會將網關流控規則轉化為熱點參數規則(ParamFlowRule),存儲在?GatewayRuleManager?中,與正常的熱點參數規則相隔離。轉換時 Sentinel 會根據請求屬性配置,為網關流控規則設置參數索引(idx),并同步到生成的熱點參數規則中。
外部請求進入 API Gateway 時會經過 Sentinel 實現的 filter,其中會依次進行?路由/API 分組匹配、請求屬性解析和參數組裝。Sentinel 會根據配置的網關流控規則來解析請求屬性,并依照參數索引順序組裝參數數組,最終傳入?SphU.entry(res, args)?中。Sentinel API Gateway Adapter Common 模塊向 Slot Chain 中添加了一個?GatewayFlowSlot,專門用來做網關規則的檢查。GatewayFlowSlot?會從?GatewayRuleManager?中提取生成的熱點參數規則,根據傳入的參數依次進行規則檢查。若某條規則不針對請求屬性,則會在參數最后一個位置置入預設的常量,達到普通流控的效果。