Spring Boot 3 整合 Spring Cloud Gateway實踐過程
引子
當前微服務架構已成為中大型系統(tǒng)的標配,但在享受拆分帶來的敏捷性時,流量治理與安全管控的復雜度也呈指數級上升。因此,我們需要構建微服務網關來為系統(tǒng)“保駕護航”。本文將會通過一個項目(核心模塊包含 鑒權服務、文件服務、主服務 共 3 個微服務),采用 Spring Cloud Alibaba 2023.0.0.0 版本技術棧(核心組件:Nacos 2.5.0 注冊中心與配置中心),分享如何構建一個微服務網關。
為什么需要微服務網關
我們當前模擬的這個項目中包含了三個業(yè)務服務,如果部署到線上的話,每個服務都有自己的ip(或域名)以及端口號。因此,我們的業(yè)務入口是分散的且暴露在外的,我們無法統(tǒng)一攔截異常流量以及限制接口訪問等。但有了微服務網關,我們就可以將所有的請求都先集中在網關這里(有點類似于一個房子的大門口),由網關對所有請求進行統(tǒng)一的管理。

實踐
在知曉了網關的作用后,我們將實踐如何在一個現成的微服務項目中整合gateway網關以及做功能開發(fā)。當然,在這之前,我們需要先完成整合。首先,我們需要建一個網關模塊,如下:

完成模塊的創(chuàng)建后,導入gateway相關的依賴,如下:
<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則是因為網關本身也是一個可以訪問的服務,所以需要引入;gateway則是這里需要使用的網關的依賴。然后來對它進行基礎的配置,如下:
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來管理服務,網關自然也是一個服務,因此也需要把它注冊到nacos。
1.統(tǒng)一路由
引入網關的首要作用是統(tǒng)一訪問的入口,所有的服務訪問都要先經過網關。因此,第一個要實現的功能就是統(tǒng)一路由。而它的實現也是非常簡單,只需要在配置文件中做下簡單配置即可:
spring:
gateway:
discovery:
locator:
enabled: true # 開啟從注冊中心動態(tài)創(chuàng)建路由的功能,利用微服務名進行路由
routes: # 路由配置信息(數組/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: # 允許跨域的相關配置
cors-configurations:
'[/**]':
allowedOriginPatterns: "*"
allowedHeaders: "*"
allowedMethods: "*"
allowCredentials: true這里對routes下的相關配置說明下:id是給每個服務的路由一個唯一編號,保證唯一即可,通常我們采用的寫法是服務名+route;uri則是服務名稱,如果寫成ip或者域名,那么地址發(fā)生變化,我們還需要重新修改配置,但是服務名稱是可以固定不變的;接下來是predicates,它可以配置多個值,我們一個服務里會有多個controller,把每個controller的路由配置在這里即可,/**表示指定的controller下的所有方法。
另外,如果負載均衡這個寫法無法被識別,說明你當前使用的spring-cloud版本中默認并不包含相關依賴,我們需要手動引入它。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>完成上述配置后,我們此時其他服務的API將無法直接訪問,而統(tǒng)一通過網關來訪問。例如原本main-service中的127.0.0.1:88/m/hello 變成了 127.0.0.1:1000/m/hello。
2.限流防刷
提到網關,一個繞不開的話題就是限流。如果有人惡意刷我們的接口,我們就需要對某些IP進行訪問限制,比如在XX秒內訪問同一接口超過XX次,就需要限制訪問。它的實現非常簡單,聲明一個處理類繼承gateway的相關過濾接口即可。代碼如下:
@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請求次數的判斷
*
* @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;
// 判斷當前ip的剩余時間,如果大于0,則表示還處于黑名單
long limitLeftTimes = redis.ttl(ipRedisLimitKey);
if ( limitLeftTimes > 0 ) {
return renderErrorMsg(exchange, ResponseStatusEnum.SYSTEM_ERROR_BLACK_IP);
}
// 在redis中更新次數
long requestCounts = redis.increment(ipRedisKey, 1);
// 如果第一次訪問,就需要設置間隔時間
if (requestCounts == 1) {
redis.expire(ipRedisKey, timeInterval);
}
// 如果還能獲得正常請求次數,說明用戶的正常請求落在正常時間內,超過則限制
if (requestCounts > continueCounts) {
redis.set(ipRedisLimitKey, ipRedisLimitKey, limitTimes);
return renderErrorMsg(exchange, ResponseStatusEnum.SYSTEM_ERROR_BLACK_IP);
}
// 放行請求
return chain.filter(exchange);
}
//過濾器的順序,數字越小優(yōu)先級越大.
@Override
public int getOrder() {
return 1;
}我們需要借助redis來實現根據時間對指定ip的控制,這里的邏輯是:如果某個ip在30秒訪問超過三次,就限制訪問,如果限制了,則20秒后再恢復。
3.登錄鑒權
關于登錄鑒權,我們目前通常會采用無狀態(tài)的做法:即用戶登錄后,后端返回token給前端,前端后續(xù)所有的請求都在headers中攜帶token,后端服務不存儲token,只對前端發(fā)來的token進行校驗和解析。而網關作為所有服務的入口,自然而然地也就可以承擔起這個職責了。
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);
}
//過濾器的順序,數字越小優(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.構建jsonResult
GraceJSONResult jsonResult = GraceJSONResult.exception(statusEnum);
//3.設置header類型
if (!response.getHeaders().containsKey("Content-Type")) {
response.getHeaders().add("Content-Type",
MimeTypeUtils.APPLICATION_JSON_VALUE);
}
//4.設置狀態(tài)碼
response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
//5.轉換json并向response寫數據
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是否一致。這里需要關注下過濾器的順序,目前的案例中我們已經編寫了兩個過濾器-限流防刷和登錄鑒權。所以可以把登錄鑒權過濾器的執(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í)行順序后,內部的驗證邏輯根據自己實際情況填寫,我這里沒用鑒權框架只是方便講解,要用也很簡單,引入之后把相關的鑒權邏輯寫進對應的過濾器就行。
小結
在本文中,我們完成了Spring Cloud Gateway微服務網關的整合,并完成了三個最基礎常見的實踐場景。如果你的項目有更多的業(yè)務需求,只需要加相應的過濾器并制定好過濾器的執(zhí)行順序即可,希望對大家有所幫助!
到此這篇關于Spring Boot 3 整合 Spring Cloud Gateway實踐過程的文章就介紹到這了,更多相關Spring Boot 整合 Spring Cloud Gateway 內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
解決Unable to start embedded container&nbs
這篇文章主要介紹了解決Unable to start embedded container SpringBoot啟動報錯問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-07-07

