Spring Boot 3 整合 Spring Cloud Gateway實踐過程
引子
當(dāng)前微服務(wù)架構(gòu)已成為中大型系統(tǒng)的標(biāo)配,但在享受拆分帶來的敏捷性時,流量治理與安全管控的復(fù)雜度也呈指數(shù)級上升。因此,我們需要構(gòu)建微服務(wù)網(wǎng)關(guān)來為系統(tǒng)“保駕護航”。本文將會通過一個項目(核心模塊包含 鑒權(quán)服務(wù)、文件服務(wù)、主服務(wù) 共 3 個微服務(wù)),采用 Spring Cloud Alibaba 2023.0.0.0 版本技術(shù)棧(核心組件:Nacos 2.5.0 注冊中心與配置中心),分享如何構(gòu)建一個微服務(wù)網(wǎng)關(guān)。
為什么需要微服務(wù)網(wǎng)關(guān)
我們當(dāng)前模擬的這個項目中包含了三個業(yè)務(wù)服務(wù),如果部署到線上的話,每個服務(wù)都有自己的ip(或域名)以及端口號。因此,我們的業(yè)務(wù)入口是分散的且暴露在外的,我們無法統(tǒng)一攔截異常流量以及限制接口訪問等。但有了微服務(wù)網(wǎng)關(guān),我們就可以將所有的請求都先集中在網(wǎng)關(guān)這里(有點類似于一個房子的大門口),由網(wǎng)關(guān)對所有請求進行統(tǒng)一的管理。
實踐
在知曉了網(wǎng)關(guān)的作用后,我們將實踐如何在一個現(xiàn)成的微服務(wù)項目中整合gateway網(wǎng)關(guān)以及做功能開發(fā)。當(dāng)然,在這之前,我們需要先完成整合。首先,我們需要建一個網(wǎng)關(guān)模塊,如下:
完成模塊的創(chuàng)建后,導(dǎo)入gateway相關(guān)的依賴,如下:
<dependencies> <dependency> <groupId>com.pitayafruits</groupId> <artifactId>wechat-pojo</artifactId> <version>1.0-SNAPSHOT</version> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> </dependencies>
說明一下:這里引入的pojo
包含了項目中常用的方法、工具類等;web
則是因為網(wǎng)關(guān)本身也是一個可以訪問的服務(wù),所以需要引入;gateway
則是這里需要使用的網(wǎng)關(guān)的依賴。然后來對它進行基礎(chǔ)的配置,如下:
server: port: 1000 tomcat: uri-encoding: UTF-8 max-swallow-size: -1 # 不限制請求體大小 spring: application: name: gateway cloud: nacos: config: server-addr: 127.0.0.1:8848 username: nacos password: naocs # 日志級別 logging: level: root: info
我們使用了nacos
來管理服務(wù),網(wǎng)關(guān)自然也是一個服務(wù),因此也需要把它注冊到nacos
。
1.統(tǒng)一路由
引入網(wǎng)關(guān)的首要作用是統(tǒng)一訪問的入口,所有的服務(wù)訪問都要先經(jīng)過網(wǎng)關(guān)。因此,第一個要實現(xiàn)的功能就是統(tǒng)一路由。而它的實現(xiàn)也是非常簡單,只需要在配置文件中做下簡單配置即可:
spring: gateway: discovery: locator: enabled: true # 開啟從注冊中心動態(tài)創(chuàng)建路由的功能,利用微服務(wù)名進行路由 routes: # 路由配置信息(數(shù)組/list) - id: authRoute # 每項路由規(guī)則都有一個唯一的id編號,可以自定義 uri: lb://auth-service # lb=負載均衡,會動態(tài)尋址 predicates: - Path=/a/** - id: fileRoute uri: lb://file-service predicates: - Path=/f/** - id: mainRoute uri: lb://main-service predicates: - Path=/m/** globalcors: # 允許跨域的相關(guān)配置 cors-configurations: '[/**]': allowedOriginPatterns: "*" allowedHeaders: "*" allowedMethods: "*" allowCredentials: true
這里對routes
下的相關(guān)配置說明下:id
是給每個服務(wù)的路由一個唯一編號,保證唯一即可,通常我們采用的寫法是服務(wù)名+route
;uri則是服務(wù)名稱,如果寫成ip或者域名,那么地址發(fā)生變化,我們還需要重新修改配置,但是服務(wù)名稱是可以固定不變的;接下來是predicates
,它可以配置多個值,我們一個服務(wù)里會有多個controller
,把每個controller
的路由配置在這里即可,/**
表示指定的controller
下的所有方法。
另外,如果負載均衡這個寫法無法被識別,說明你當(dāng)前使用的spring-cloud
版本中默認并不包含相關(guān)依賴,我們需要手動引入它。
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency>
完成上述配置后,我們此時其他服務(wù)的API將無法直接訪問,而統(tǒng)一通過網(wǎng)關(guān)來訪問。例如原本main-service
中的127.0.0.1:88/m/hello
變成了 127.0.0.1:1000/m/hello
。
2.限流防刷
提到網(wǎng)關(guān),一個繞不開的話題就是限流。如果有人惡意刷我們的接口,我們就需要對某些IP進行訪問限制,比如在XX秒內(nèi)訪問同一接口超過XX次,就需要限制訪問。它的實現(xiàn)非常簡單,聲明一個處理類繼承g(shù)ateway的相關(guān)過濾接口即可。代碼如下:
@Component public class IPLimitFilter implements GlobalFilter { private static final Integer continueCounts = 3; private static final Integer timeInterval = 20; private static final Integer limitTimes = 30; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { return doLimit(exchange, chain); } /** * 限制ip請求次數(shù)的判斷 * * @param exchange 請求交換器 * @param chain 過濾器鏈 * @return 返回值 */ public Mono<Void> doLimit(ServerWebExchange exchange, GatewayFilterChain chain) { // 獲取ip ServerHttpRequest request = exchange.getRequest(); String ip = IPUtil.getIP(request); // 正常ip定義 final String ipRedisKey = "gateway-ip" + ip; // 被攔截的黑名單,如果在redis中存在,那么就不允許訪問 final String ipRedisLimitKey = "gateway-ip:limit" + ip; // 判斷當(dāng)前ip的剩余時間,如果大于0,則表示還處于黑名單 long limitLeftTimes = redis.ttl(ipRedisLimitKey); if ( limitLeftTimes > 0 ) { return renderErrorMsg(exchange, ResponseStatusEnum.SYSTEM_ERROR_BLACK_IP); } // 在redis中更新次數(shù) long requestCounts = redis.increment(ipRedisKey, 1); // 如果第一次訪問,就需要設(shè)置間隔時間 if (requestCounts == 1) { redis.expire(ipRedisKey, timeInterval); } // 如果還能獲得正常請求次數(shù),說明用戶的正常請求落在正常時間內(nèi),超過則限制 if (requestCounts > continueCounts) { redis.set(ipRedisLimitKey, ipRedisLimitKey, limitTimes); return renderErrorMsg(exchange, ResponseStatusEnum.SYSTEM_ERROR_BLACK_IP); } // 放行請求 return chain.filter(exchange); } //過濾器的順序,數(shù)字越小優(yōu)先級越大. @Override public int getOrder() { return 1; }
我們需要借助redis來實現(xiàn)根據(jù)時間對指定ip的控制,這里的邏輯是:如果某個ip在30秒訪問超過三次,就限制訪問,如果限制了,則20秒后再恢復(fù)。
3.登錄鑒權(quán)
關(guān)于登錄鑒權(quán),我們目前通常會采用無狀態(tài)
的做法:即用戶登錄后,后端返回token給前端,前端后續(xù)所有的請求都在headers
中攜帶token,后端服務(wù)不存儲token,只對前端發(fā)來的token進行校驗和解析。而網(wǎng)關(guān)作為所有服務(wù)的入口,自然而然地也就可以承擔(dān)起這個職責(zé)了。
import com.google.gson.Gson; import com.pitayafruits.base.BaseInfoProperties; import com.pitayafruits.grace.result.GraceJSONResult; import com.pitayafruits.grace.result.ResponseStatusEnum; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.stereotype.Component; import org.springframework.util.AntPathMatcher; import org.springframework.util.MimeTypeUtils; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.nio.charset.StandardCharsets; import java.util.List; @Component @Slf4j @RefreshScope public class SecurityFilterToken extends BaseInfoProperties implements GlobalFilter, Ordered { @Resource private ExcludeUrlProperties excludeUrlProperties; private AntPathMatcher antPathMatcher = new AntPathMatcher(); @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 獲取用戶請求路徑 String url = exchange.getRequest().getURI().getPath(); // 獲取所有需要排除校驗的url List<String> excludeList = excludeUrlProperties.getUrls(); // 校驗并排除url if (excludeList != null && !excludeList.isEmpty()) { for (String excludeUrl : excludeList) { if (antPathMatcher.matchStart(excludeUrl, url)) { return chain.filter(exchange); } } } // 從header中獲得用戶id和token String userId = exchange.getRequest().getHeaders().getFirst(HEADER_USER_ID); String userToken = exchange.getRequest().getHeaders().getFirst(HEADER_USER_TOKEN); // 校驗header中的token if (StringUtils.isNotBlank(userId) && StringUtils.isNotBlank(userToken)) { String redisToken = redis.get(REDIS_USER_TOKEN + ":" + userId); if (redisToken.equals(userToken)) { return chain.filter(exchange); } } // 默認不放行 return renderErrorMsg(exchange, ResponseStatusEnum.UN_LOGIN); } //過濾器的順序,數(shù)字越小優(yōu)先級越大. @Override public int getOrder() { return 0; } /** * 異常信息包裝 * * @param exchange 交換器 * @param statusEnum 狀態(tài)枚舉 * @return 返回值 */ public Mono<Void> renderErrorMsg(ServerWebExchange exchange, ResponseStatusEnum statusEnum) { //1.獲得response ServerHttpResponse response = exchange.getResponse(); //2.構(gòu)建jsonResult GraceJSONResult jsonResult = GraceJSONResult.exception(statusEnum); //3.設(shè)置header類型 if (!response.getHeaders().containsKey("Content-Type")) { response.getHeaders().add("Content-Type", MimeTypeUtils.APPLICATION_JSON_VALUE); } //4.設(shè)置狀態(tài)碼 response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); //5.轉(zhuǎn)換json并向response寫數(shù)據(jù) String resultJson = new Gson().toJson(jsonResult); DataBuffer buffer = response.bufferFactory().wrap(resultJson.getBytes(StandardCharsets.UTF_8)); //6.返回 return response.writeWith(Mono.just(buffer)); } }
在我這個示例中,我做的校驗邏輯很簡單:只是用戶登錄的時候會在redis
里存放生成的token
,然后其他接口訪問的時候比對下傳來的token
和redis
里存放的token
是否一致。這里需要關(guān)注下過濾器的順序,目前的案例中我們已經(jīng)編寫了兩個過濾器-限流防刷和登錄鑒權(quán)。所以可以把登錄鑒權(quán)過濾器的執(zhí)行順序改為0,限流防抖改為1。
另外,我們需要對部分接口放行不攔截,比如登錄接口。而我這里的做法則是將放行接口寫在配置文件里,并聲明配置類進行讀取。
exclude.urls[0] = /passport/getSMSCode exclude.urls[1] = /passport/regist exclude.urls[2] = /passport/login
import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.PropertySource; import org.springframework.stereotype.Component; import java.util.List; @Component @Data @PropertySource("classpath:excludeUrlPath.properties") @ConfigurationProperties(prefix = "exclude") public class ExcludeUrlProperties { private List<String> urls; }
特別說明下:這里制定好過濾器的執(zhí)行順序后,內(nèi)部的驗證邏輯根據(jù)自己實際情況填寫,我這里沒用鑒權(quán)框架只是方便講解,要用也很簡單,引入之后把相關(guān)的鑒權(quán)邏輯寫進對應(yīng)的過濾器就行。
小結(jié)
在本文中,我們完成了Spring Cloud Gateway
微服務(wù)網(wǎng)關(guān)的整合,并完成了三個最基礎(chǔ)常見的實踐場景。如果你的項目有更多的業(yè)務(wù)需求,只需要加相應(yīng)的過濾器并制定好過濾器的執(zhí)行順序即可,希望對大家有所幫助!
到此這篇關(guān)于Spring Boot 3 整合 Spring Cloud Gateway實踐過程的文章就介紹到這了,更多相關(guān)Spring Boot 整合 Spring Cloud Gateway 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決Unable to start embedded container&nbs
這篇文章主要介紹了解決Unable to start embedded container SpringBoot啟動報錯問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-07-07Springmvc請求參數(shù)類型轉(zhuǎn)換器及原生api代碼實例
這篇文章主要介紹了Springmvc請求參數(shù)類型轉(zhuǎn)換器及原生api代碼實例,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-10-10java利用pdfbox+poi往pdf插入數(shù)據(jù)
這篇文章主要給大家介紹了關(guān)于java利用pdfbox+poi如何往pdf插入數(shù)據(jù)的相關(guān)資料,文中通過實例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2022-02-02