Spring Boot 3 整合 Spring Cloud Gateway實(shí)踐過(guò)程
引子
當(dāng)前微服務(wù)架構(gòu)已成為中大型系統(tǒng)的標(biāo)配,但在享受拆分帶來(lái)的敏捷性時(shí),流量治理與安全管控的復(fù)雜度也呈指數(shù)級(jí)上升。因此,我們需要構(gòu)建微服務(wù)網(wǎng)關(guān)來(lái)為系統(tǒng)“保駕護(hù)航”。本文將會(huì)通過(guò)一個(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ù)入口是分散的且暴露在外的,我們無(wú)法統(tǒng)一攔截異常流量以及限制接口訪問(wèn)等。但有了微服務(wù)網(wǎng)關(guān),我們就可以將所有的請(qǐng)求都先集中在網(wǎng)關(guān)這里(有點(diǎn)類(lèi)似于一個(gè)房子的大門(mén)口),由網(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)以及做功能開(kāi)發(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>說(shuō)明一下:這里引入的pojo包含了項(xiàng)目中常用的方法、工具類(lèi)等;web則是因?yàn)榫W(wǎng)關(guān)本身也是一個(gè)可以訪問(wèn)的服務(wù),所以需要引入;gateway則是這里需要使用的網(wǎng)關(guān)的依賴。然后來(lái)對(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來(lái)管理服務(wù),網(wǎng)關(guān)自然也是一個(gè)服務(wù),因此也需要把它注冊(cè)到nacos。
1.統(tǒng)一路由
引入網(wǎng)關(guān)的首要作用是統(tǒng)一訪問(wèn)的入口,所有的服務(wù)訪問(wèn)都要先經(jīng)過(guò)網(wǎng)關(guān)。因此,第一個(gè)要實(shí)現(xiàn)的功能就是統(tǒng)一路由。而它的實(shí)現(xiàn)也是非常簡(jiǎn)單,只需要在配置文件中做下簡(jiǎn)單配置即可:
spring:
gateway:
discovery:
locator:
enabled: true # 開(kāi)啟從注冊(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)配置說(shuō)明下:id是給每個(gè)服務(wù)的路由一個(gè)唯一編號(hào),保證唯一即可,通常我們采用的寫(xiě)法是服務(wù)名+route;uri則是服務(wù)名稱,如果寫(xiě)成ip或者域名,那么地址發(fā)生變化,我們還需要重新修改配置,但是服務(wù)名稱是可以固定不變的;接下來(lái)是predicates,它可以配置多個(gè)值,我們一個(gè)服務(wù)里會(huì)有多個(gè)controller,把每個(gè)controller的路由配置在這里即可,/**表示指定的controller下的所有方法。
另外,如果負(fù)載均衡這個(gè)寫(xiě)法無(wú)法被識(shí)別,說(shuō)明你當(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將無(wú)法直接訪問(wèn),而統(tǒng)一通過(guò)網(wǎng)關(guān)來(lái)訪問(wèn)。例如原本main-service中的127.0.0.1:88/m/hello 變成了 127.0.0.1:1000/m/hello。
2.限流防刷
提到網(wǎng)關(guān),一個(gè)繞不開(kāi)的話題就是限流。如果有人惡意刷我們的接口,我們就需要對(duì)某些IP進(jìn)行訪問(wèn)限制,比如在XX秒內(nèi)訪問(wèn)同一接口超過(guò)XX次,就需要限制訪問(wèn)。它的實(shí)現(xiàn)非常簡(jiǎn)單,聲明一個(gè)處理類(lèi)繼承g(shù)ateway的相關(guān)過(guò)濾接口即可。代碼如下:
@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 過(guò)濾器鏈
* @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中存在,那么就不允許訪問(wèn)
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);
// 如果第一次訪問(wèn),就需要設(shè)置間隔時(shí)間
if (requestCounts == 1) {
redis.expire(ipRedisKey, timeInterval);
}
// 如果還能獲得正常請(qǐng)求次數(shù),說(shuō)明用戶的正常請(qǐng)求落在正常時(shí)間內(nèi),超過(guò)則限制
if (requestCounts > continueCounts) {
redis.set(ipRedisLimitKey, ipRedisLimitKey, limitTimes);
return renderErrorMsg(exchange, ResponseStatusEnum.SYSTEM_ERROR_BLACK_IP);
}
// 放行請(qǐng)求
return chain.filter(exchange);
}
//過(guò)濾器的順序,數(shù)字越小優(yōu)先級(jí)越大.
@Override
public int getOrder() {
return 1;
}我們需要借助redis來(lái)實(shí)現(xiàn)根據(jù)時(shí)間對(duì)指定ip的控制,這里的邏輯是:如果某個(gè)ip在30秒訪問(wèn)超過(guò)三次,就限制訪問(wèn),如果限制了,則20秒后再恢復(fù)。
3.登錄鑒權(quán)
關(guān)于登錄鑒權(quán),我們目前通常會(huì)采用無(wú)狀態(tài)的做法:即用戶登錄后,后端返回token給前端,前端后續(xù)所有的請(qǐng)求都在headers中攜帶token,后端服務(wù)不存儲(chǔ)token,只對(duì)前端發(fā)來(lái)的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);
}
//過(guò)濾器的順序,數(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類(lèi)型
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寫(xiě)數(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)邏輯很簡(jiǎn)單:只是用戶登錄的時(shí)候會(huì)在redis里存放生成的token,然后其他接口訪問(wèn)的時(shí)候比對(duì)下傳來(lái)的token和redis里存放的token是否一致。這里需要關(guān)注下過(guò)濾器的順序,目前的案例中我們已經(jīng)編寫(xiě)了兩個(gè)過(guò)濾器-限流防刷和登錄鑒權(quán)。所以可以把登錄鑒權(quán)過(guò)濾器的執(zhí)行順序改為0,限流防抖改為1。
另外,我們需要對(duì)部分接口放行不攔截,比如登錄接口。而我這里的做法則是將放行接口寫(xiě)在配置文件里,并聲明配置類(lèi)進(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;
}特別說(shuō)明下:這里制定好過(guò)濾器的執(zhí)行順序后,內(nèi)部的驗(yàn)證邏輯根據(jù)自己實(shí)際情況填寫(xiě),我這里沒(méi)用鑒權(quán)框架只是方便講解,要用也很簡(jiǎn)單,引入之后把相關(guān)的鑒權(quán)邏輯寫(xiě)進(jìn)對(duì)應(yīng)的過(guò)濾器就行。
小結(jié)
在本文中,我們完成了Spring Cloud Gateway微服務(wù)網(wǎng)關(guān)的整合,并完成了三個(gè)最基礎(chǔ)常見(jiàn)的實(shí)踐場(chǎng)景。如果你的項(xiàng)目有更多的業(yè)務(wù)需求,只需要加相應(yīng)的過(guò)濾器并制定好過(guò)濾器的執(zhí)行順序即可,希望對(duì)大家有所幫助!
到此這篇關(guān)于Spring Boot 3 整合 Spring Cloud Gateway實(shí)踐過(guò)程的文章就介紹到這了,更多相關(guān)Spring Boot 整合 Spring Cloud Gateway 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Spring Cloud服務(wù)入口Gateway的介紹和使用問(wèn)題小結(jié)
- spring?cloud?gateway限流常見(jiàn)算法實(shí)現(xiàn)
- SpringCloud的網(wǎng)關(guān)Zuul和Gateway詳解
- SpringCloud Zuul和Gateway的實(shí)例代碼(搭建方式)
- 一文掌握spring cloud gateway(總結(jié)篇)
- SpringCloudGateway 網(wǎng)關(guān)登錄校驗(yàn)實(shí)現(xiàn)思路
- Spring?Cloud?Gateway服務(wù)網(wǎng)關(guān)限流問(wèn)題及解決
相關(guān)文章
Java實(shí)現(xiàn)驗(yàn)證文件名有效性的方法詳解
在本文中,我們將討論使用?Java?驗(yàn)證一個(gè)給定的字符串是否具有操作系統(tǒng)的有效文件名的不同方法,文中的示例代碼講解詳細(xì),感興趣的可以了解一下2022-09-09
java?http請(qǐng)求設(shè)置代理Proxy的兩種常見(jiàn)方法
代理是一種常見(jiàn)的設(shè)計(jì)模式,其目的就是為其他對(duì)象提供一個(gè)代理以控制對(duì)某個(gè)對(duì)象的訪問(wèn),這篇文章主要給大家介紹了關(guān)于java?http請(qǐng)求設(shè)置代理Proxy的兩種常見(jiàn)方法,需要的朋友可以參考下2023-11-11
在IntelliJ IDEA中.idea文件是什么可以刪除嗎
相信有很多小伙伴,在用idea寫(xiě)java代碼的時(shí)候,創(chuàng)建工程總是會(huì)出現(xiàn).idea文件,該文件也從來(lái)沒(méi)去打開(kāi)使用過(guò),那么它在我們項(xiàng)目里面,扮演什么角色,到底能不能刪除它呢?這篇文章主要介紹了在IntelliJ IDEA中.idea文件是什么可以刪除嗎,需要的朋友可以參考下2024-01-01
如何使用BeanUtils.copyProperties進(jìn)行對(duì)象之間的屬性賦值
這篇文章主要介紹了使用BeanUtils.copyProperties進(jìn)行對(duì)象之間的屬性賦值,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05
基于SSM框架+Javamail發(fā)送郵件的代碼實(shí)例
本篇文章主要介紹了基于SSM框架+Javamail發(fā)送郵件的代碼實(shí)例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2016-12-12
Java?Thread?類(lèi)和Runnable?接口詳解
這篇文章主要介紹了Java?Thread?類(lèi)和Runnable接口詳解,文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的朋友可以參考一下2022-08-08
SpringBoot中Bean拷貝及工具類(lèi)封裝的實(shí)現(xiàn)
本文主要介紹了SpringBoot中Bean拷貝及工具類(lèi)封裝的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05

