Spring Boot 3 整合 Spring Cloud Gateway實(shí)踐過程
引子
當(dāng)前微服務(wù)架構(gòu)已成為中大型系統(tǒng)的標(biāo)配,但在享受拆分帶來的敏捷性時(shí),流量治理與安全管控的復(fù)雜度也呈指數(shù)級(jí)上升。因此,我們需要構(gòu)建微服務(wù)網(wǎng)關(guān)來為系統(tǒng)“保駕護(hù)航”。本文將會(huì)通過一個(gè)項(xiàng)目(核心模塊包含 鑒權(quán)服務(wù)、文件服務(wù)、主服務(wù) 共 3 個(gè)微服務(wù)),采用 Spring Cloud Alibaba 2023.0.0.0 版本技術(shù)棧(核心組件:Nacos 2.5.0 注冊(cè)中心與配置中心),分享如何構(gòu)建一個(gè)微服務(wù)網(wǎng)關(guān)。
為什么需要微服務(wù)網(wǎng)關(guān)
我們當(dāng)前模擬的這個(gè)項(xiàng)目中包含了三個(gè)業(yè)務(wù)服務(wù),如果部署到線上的話,每個(gè)服務(wù)都有自己的ip(或域名)以及端口號(hào)。因此,我們的業(yè)務(wù)入口是分散的且暴露在外的,我們無法統(tǒng)一攔截異常流量以及限制接口訪問等。但有了微服務(wù)網(wǎng)關(guān),我們就可以將所有的請(qǐng)求都先集中在網(wǎng)關(guān)這里(有點(diǎn)類似于一個(gè)房子的大門口),由網(wǎng)關(guān)對(duì)所有請(qǐng)求進(jìn)行統(tǒng)一的管理。
實(shí)踐
在知曉了網(wǎng)關(guān)的作用后,我們將實(shí)踐如何在一個(gè)現(xiàn)成的微服務(wù)項(xiàng)目中整合gateway網(wǎng)關(guān)以及做功能開發(fā)。當(dāng)然,在這之前,我們需要先完成整合。首先,我們需要建一個(gè)網(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
包含了項(xiàng)目中常用的方法、工具類等;web
則是因?yàn)榫W(wǎng)關(guān)本身也是一個(gè)可以訪問的服務(wù),所以需要引入;gateway
則是這里需要使用的網(wǎng)關(guān)的依賴。然后來對(duì)它進(jìn)行基礎(chǔ)的配置,如下:
server: port: 1000 tomcat: uri-encoding: UTF-8 max-swallow-size: -1 # 不限制請(qǐng)求體大小 spring: application: name: gateway cloud: nacos: config: server-addr: 127.0.0.1:8848 username: nacos password: naocs # 日志級(jí)別 logging: level: root: info
我們使用了nacos
來管理服務(wù),網(wǎng)關(guān)自然也是一個(gè)服務(wù),因此也需要把它注冊(cè)到nacos
。
1.統(tǒng)一路由
引入網(wǎng)關(guān)的首要作用是統(tǒng)一訪問的入口,所有的服務(wù)訪問都要先經(jīng)過網(wǎng)關(guān)。因此,第一個(gè)要實(shí)現(xiàn)的功能就是統(tǒng)一路由。而它的實(shí)現(xiàn)也是非常簡單,只需要在配置文件中做下簡單配置即可:
spring: gateway: discovery: locator: enabled: true # 開啟從注冊(cè)中心動(dòng)態(tài)創(chuàng)建路由的功能,利用微服務(wù)名進(jìn)行路由 routes: # 路由配置信息(數(shù)組/list) - id: authRoute # 每項(xiàng)路由規(guī)則都有一個(gè)唯一的id編號(hào),可以自定義 uri: lb://auth-service # lb=負(fù)載均衡,會(huì)動(dòng)態(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
這里對(duì)routes
下的相關(guān)配置說明下:id
是給每個(gè)服務(wù)的路由一個(gè)唯一編號(hào),保證唯一即可,通常我們采用的寫法是服務(wù)名+route
;uri則是服務(wù)名稱,如果寫成ip或者域名,那么地址發(fā)生變化,我們還需要重新修改配置,但是服務(wù)名稱是可以固定不變的;接下來是predicates
,它可以配置多個(gè)值,我們一個(gè)服務(wù)里會(huì)有多個(gè)controller
,把每個(gè)controller
的路由配置在這里即可,/**
表示指定的controller
下的所有方法。
另外,如果負(fù)載均衡這個(gè)寫法無法被識(shí)別,說明你當(dāng)前使用的spring-cloud
版本中默認(rèn)并不包含相關(guān)依賴,我們需要手動(dòng)引入它。
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency>
完成上述配置后,我們此時(shí)其他服務(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),一個(gè)繞不開的話題就是限流。如果有人惡意刷我們的接口,我們就需要對(duì)某些IP進(jìn)行訪問限制,比如在XX秒內(nèi)訪問同一接口超過XX次,就需要限制訪問。它的實(shí)現(xiàn)非常簡單,聲明一個(gè)處理類繼承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請(qǐng)求次數(shù)的判斷 * * @param exchange 請(qǐng)求交換器 * @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的剩余時(shí)間,如果大于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è)置間隔時(shí)間 if (requestCounts == 1) { redis.expire(ipRedisKey, timeInterval); } // 如果還能獲得正常請(qǐng)求次數(shù),說明用戶的正常請(qǐng)求落在正常時(shí)間內(nèi),超過則限制 if (requestCounts > continueCounts) { redis.set(ipRedisLimitKey, ipRedisLimitKey, limitTimes); return renderErrorMsg(exchange, ResponseStatusEnum.SYSTEM_ERROR_BLACK_IP); } // 放行請(qǐng)求 return chain.filter(exchange); } //過濾器的順序,數(shù)字越小優(yōu)先級(jí)越大. @Override public int getOrder() { return 1; }
我們需要借助redis來實(shí)現(xiàn)根據(jù)時(shí)間對(duì)指定ip的控制,這里的邏輯是:如果某個(gè)ip在30秒訪問超過三次,就限制訪問,如果限制了,則20秒后再恢復(fù)。
3.登錄鑒權(quán)
關(guān)于登錄鑒權(quán),我們目前通常會(huì)采用無狀態(tài)
的做法:即用戶登錄后,后端返回token給前端,前端后續(xù)所有的請(qǐng)求都在headers
中攜帶token,后端服務(wù)不存儲(chǔ)token,只對(duì)前端發(fā)來的token進(jìn)行校驗(yàn)和解析。而網(wǎng)關(guān)作為所有服務(wù)的入口,自然而然地也就可以承擔(dān)起這個(gè)職責(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) { // 獲取用戶請(qǐng)求路徑 String url = exchange.getRequest().getURI().getPath(); // 獲取所有需要排除校驗(yàn)的url List<String> excludeList = excludeUrlProperties.getUrls(); // 校驗(yàn)并排除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); // 校驗(yàn)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); } } // 默認(rèn)不放行 return renderErrorMsg(exchange, ResponseStatusEnum.UN_LOGIN); } //過濾器的順序,數(shù)字越小優(yōu)先級(jí)越大. @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)); } }
在我這個(gè)示例中,我做的校驗(yàn)邏輯很簡單:只是用戶登錄的時(shí)候會(huì)在redis
里存放生成的token
,然后其他接口訪問的時(shí)候比對(duì)下傳來的token
和redis
里存放的token
是否一致。這里需要關(guān)注下過濾器的順序,目前的案例中我們已經(jīng)編寫了兩個(gè)過濾器-限流防刷和登錄鑒權(quán)。所以可以把登錄鑒權(quán)過濾器的執(zhí)行順序改為0,限流防抖改為1。
另外,我們需要對(duì)部分接口放行不攔截,比如登錄接口。而我這里的做法則是將放行接口寫在配置文件里,并聲明配置類進(jìn)行讀取。
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)部的驗(yàn)證邏輯根據(jù)自己實(shí)際情況填寫,我這里沒用鑒權(quán)框架只是方便講解,要用也很簡單,引入之后把相關(guān)的鑒權(quán)邏輯寫進(jìn)對(duì)應(yīng)的過濾器就行。
小結(jié)
在本文中,我們完成了Spring Cloud Gateway
微服務(wù)網(wǎng)關(guān)的整合,并完成了三個(gè)最基礎(chǔ)常見的實(shí)踐場(chǎng)景。如果你的項(xiàng)目有更多的業(yè)務(wù)需求,只需要加相應(yīng)的過濾器并制定好過濾器的執(zhí)行順序即可,希望對(duì)大家有所幫助!
到此這篇關(guān)于Spring Boot 3 整合 Spring Cloud Gateway實(shí)踐過程的文章就介紹到這了,更多相關(guān)Spring Boot 整合 Spring Cloud Gateway 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java中分組統(tǒng)計(jì)的三種實(shí)現(xiàn)方式
這篇文章主要介紹了java中分組統(tǒng)計(jì)的三種實(shí)現(xiàn)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07解決Unable to start embedded container&nbs
這篇文章主要介紹了解決Unable to start embedded container SpringBoot啟動(dòng)報(bào)錯(cuò)問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07WebUploader實(shí)現(xiàn)圖片上傳功能
這篇文章主要為大家詳細(xì)介紹了WebUploader實(shí)現(xiàn)圖片上傳功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-03-03如何在Redis中實(shí)現(xiàn)分頁排序查詢過程解析
這篇文章主要介紹了如何在Redis中實(shí)現(xiàn)分頁排序查詢過程解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-07-07Java SpringBoot自動(dòng)裝配原理詳解及源碼注釋
SpringBoot的自動(dòng)裝配是拆箱即用的基礎(chǔ),也是微服務(wù)化的前提。其實(shí)它并不那么神秘,我在這之前已經(jīng)寫過最基本的實(shí)現(xiàn)了,大家可以參考這篇文章,來看看它是怎么樣實(shí)現(xiàn)的,我們透過源代碼來把握自動(dòng)裝配的來龍去脈2021-10-10Springmvc請(qǐng)求參數(shù)類型轉(zhuǎn)換器及原生api代碼實(shí)例
這篇文章主要介紹了Springmvc請(qǐng)求參數(shù)類型轉(zhuǎn)換器及原生api代碼實(shí)例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-10-10java利用pdfbox+poi往pdf插入數(shù)據(jù)
這篇文章主要給大家介紹了關(guān)于java利用pdfbox+poi如何往pdf插入數(shù)據(jù)的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2022-02-02