本篇文章基于黑馬程序員的微服務課程內容,結合個人學習過程中的理解與思考進行整理。本節將圍繞以下幾個問題展開:什么是網關和配置管理
前面那篇文章,我們了解如何把一個單體的項目拆成分布式微服務項目,并且講解一下各個服務之間是如何通信的。發現一些問題:
- 每個微服務都有不同的地址和端口,那么前端需要調用微服務的功能時,就會出現以下問題:
- 前端需要調用不同微服務的時候,地址和端口太多,不易維護
- 前端無法調用Nacos,當后端端口發送改變的時候,前端察覺不到
- 單體項目需要做登錄、權限校驗,還有為了方便用戶信息傳遞登錄時保存用戶信息,那么拆分成微服務就會出現以下問題:
- 每個微服務都需要編寫登錄校驗、用戶信息保存
- 當微服務之間的調用的時候,該如何傳遞用戶信息?
這篇文章就是解決這些問題的,也是這篇文章的主題——網關:
- 網關路由:解決前端請求路口統一的問題
- 網關鑒權:解決統一登錄校驗和用戶信息獲取問題
- 統一配置管理:解決微服務,配置文件重復的問題和配置熱更新的問題
1.網關路由
1.1 認識網關
網關(Gateway) 是微服務架構中的統一入口,它位于客戶端與服務端之間,接收所有外部請求,然后根據請求內容將其轉發到對應的后端微服務。
可以理解為:網關是微服務系統的“前門”。
從圖中可以看出,網關作為后端的一部分,承擔了統一入口的作用。前端只需請求網關,不需要關心各個微服務的具體地址。網關接收到請求后,會通過訪問 Nacos 注冊中心獲取服務的最新信息,并根據配置的路由規則將請求轉發到對應服務,同時實現負載均衡和服務發現,簡化了前端調用邏輯,也增強了系統的靈活性與可維護性。
1.2 快速實現
- 創建網關模塊
- 引入網關的依賴
- 編寫啟動類
- 編寫配置文件
1.3 配置文件
這里主要講解一下配置文件
server:port: 8080 # 網關端口,給前端的端口spring:application:name: gateway-servicecloud:nacos:discovery:server-addr: localhost:8848 # Nacos注冊中心地址gateway:routes:# 訂單服務路由- id: order_routeuri: lb://order-service # 負載均衡到訂單服務predicates:- Path=/order/** # 匹配/order開頭的請求- Method=GET,POST # 允許GET/POST請求filters:- StripPrefix=1 # 移除第一段路徑(/order)- AddRequestHeader=Gateway,true # 添加請求頭# 用戶服務路由- id: user_routeuri: lb://user-servicepredicates:- Path=/user/** # 匹配/user路徑- After=2025-01-01T00:00:00.000+08:00 # 時間生效范圍filters:- PrefixPath=/api # 添加前綴 /api/user/**
這里我們重點關注predicates
,也就是路由斷言。SpringCloudGateway中支持的斷言類型有很多:
名稱 | 說明 | 示例 |
---|---|---|
After | 是某個時間點后的請求 | - After=2037-01-20T17:42:47.789-07:00[America/Denver] |
Before | 是某個時間點之前的請求 | - Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] |
Between | 是某兩個時間點之前的請求 | - Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver] |
Cookie | 請求必須包含某些cookie | - Cookie=chocolate, ch.p |
Header | 請求必須包含某些header | - Header=X-Request-Id, \d+ |
Host | 請求必須是訪問某個host(域名) | - Host=.somehost.org,.anotherhost.org |
Method | 請求方式必須是指定方式 | - Method=GET,POST |
Path | 請求路徑必須符合指定規則 | - Path=/red/{segment},/blue/** |
Query | 請求參數必須包含指定參數 | - Query=name, Jack或者- Query=name |
RemoteAddr | 請求者的ip必須是指定范圍 | - RemoteAddr=192.168.1.1/24 |
weight | 權重處理 |
2.網關登錄校驗
2.1 思路分析(JWT為例)
我們看到這張圖網關需要完成的工作為:
- JWT信息校驗
- 將用戶信息傳遞到下游
- 然后微服務之間傳遞用戶信息
2.2 網關過濾器
首先,登錄校驗一定要放在轉發路由之前去完成,因此我們需要先了解網關是如何進行工作的。
- 接受前端的請求,
HandlerMapper
,進行路由匹配 - 然后經過一系列的過濾器鏈
- 路徑處理過濾器
- 添加信息頭處理器
- 自定義處理器
- 等等
- 路由轉發到相應的微服務中
- 微服務返回結果再次經過過處理器鏈
我們需要做的就是在轉發到微服務之前進行登錄校驗,就是自定義一個網關過濾器。
網關過濾器鏈中的過濾器有兩種:
GatewayFilter
: 路由過濾器,作用范圍比較靈活,可以是任意指定的路由RouteGlobalFilter
:全局過濾器,作用范圍是所有路由,不可配置。
如何理解這個不可配置是什么意思? 意思就是不可以通過配置文件動態配置
2.2.1 基于全局過濾器實現登錄校驗
我們就自定義一個基于全局過濾器的登錄校驗過濾器
1. 創建自定義全局過濾器
我們創建一個LoginCheckGlobalFilter
類,實現GlobalFilter
和Ordered
接口:
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;@Component
public class LoginCheckGlobalFilter implements GlobalFilter, Ordered {// 白名單路徑(不需要登錄校驗的路徑)private static final List<String> WHITE_LIST = Arrays.asList("/api/user/login","/api/user/register","/api/doc.html","/api/swagger-ui.html");@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 1. 獲取請求路徑String path = exchange.getRequest().getPath().toString();// 2. 檢查是否在白名單中if (WHITE_LIST.contains(path)) {return chain.filter(exchange); // 直接放行}// 3. 獲取tokenString token = exchange.getRequest().getHeaders().getFirst("Authorization");// 4. 校驗tokenif (StringUtils.isBlank(token) || !validateToken(token)) {// 未登錄或token無效exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);return exchange.getResponse().setComplete();}// 5. token有效,放行請求return chain.filter(exchange);}@Overridepublic int getOrder() {return -1; // 設置過濾器執行順序(數值越小優先級越高)}// 模擬token校驗方法(實際項目中應調用認證服務)private boolean validateToken(String token) {// 這里簡單模擬校驗邏輯return token != null && token.startsWith("Bearer ");}
}
2. 過濾器邏輯說明
- 白名單檢查:放行登錄、注冊等不需要認證的接口
- Token獲取:從請求頭
Authorization
中獲取JWT token - Token校驗:
- 無效token:返回401 Unauthorized
- 有效token:放行請求
2.3 微服務直接如何傳遞用戶信息
現在,網關已經可以完成登錄校驗并獲取登錄用戶身份信息。但是當網關將請求轉發到微服務時,微服務又該如何獲取用戶身份呢?
由于網關發送請求到微服務依然采用的是Http
請求,因此我們可以將用戶信息以請求頭的方式傳遞到下游微服務。然后微服務可以從請求頭中獲取登錄用戶信息。考慮到微服務內部可能很多地方都需要用到登錄用戶信息,因此我們可以利用SpringMVC的攔截器來實現登錄用戶信息獲取,并存入ThreadLocal,方便后續使用。
大致流程
首先每一次請求可以看做一個線程,之前在單體項目的時候。經過登錄校驗的時候,把信息放入線程的上下文中。我微服務項目也可以根據這個方法,進行改造一下:
- 網關 → 微服務:通過 HTTP請求頭 傳遞用戶信息(如JWT解析后的用戶ID)。
- 微服務內部:通過 SpringMVC攔截器 + ThreadLocal 存儲用戶信息,避免重復解析。
- 微服務間調用(OpenFeign):通過 Feign攔截器 透傳用戶信息,確保鏈路完整。
具體步驟
1. 網關傳遞用戶信息到微服務
網關(如Spring Cloud Gateway):在登錄校驗后,將用戶信息(如userId
、username
)添加到請求頭中,轉發到下游微服務。
# 網關配置示例(Spring Cloud Gateway)
spring:cloud:gateway:default-filters:- name: AddRequestHeaderargs:name: X-User-Idvalue: "#{@userContext.getUserId()}" # 從JWT解析后注入
2. 微服務接收并存儲用戶信息
2.1 定義ThreadLocal上下文
public class UserContext {private static final ThreadLocal<Long> userIdHolder = new ThreadLocal<>();public static void setUserId(Long userId) {userIdHolder.set(userId);}public static Long getUserId() {return userIdHolder.get();}public static void clear() {userIdHolder.remove();}
}
2.2 攔截器解析請求頭
@Component
public class UserInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {String userId = request.getHeader("X-User-Id");if (userId != null) {UserContext.setUserId(Long.valueOf(userId));}return true;}@Overridepublic void afterCompletion(...) {UserContext.clear(); // 避免內存泄漏}
}
2.3 注冊攔截器
@Configuration
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate UserInterceptor userInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(userInterceptor);}
}
3. OpenFeign透傳用戶信息
微服務間調用時,需通過Feign攔截器將用戶信息附加到請求頭,確保鏈路透明。
3.1 Feign攔截器實現
java
復制
@Component
public class FeignUserInterceptor implements RequestInterceptor {@Overridepublic void apply(RequestTemplate template) {Long userId = UserContext.getUserId();if (userId != null) {template.header("X-User-Id", String.valueOf(userId));}}
}
3.2 啟用Feign攔截器
確保Feign客戶端掃描到該攔截器(通常已自動注入)。
4. 完整流程總結
- 用戶請求 → 網關:攜帶JWT令牌。
- 網關:解析JWT → 將
userId
放入請求頭 → 轉發到微服務A。 - 微服務A:
- 攔截器讀取
X-User-Id
→ 存入UserContext
(ThreadLocal)。 - 業務代碼通過
UserContext.getUserId()
獲取用戶信息。
- 攔截器讀取
- 微服務A → 微服務B(通過OpenFeign):
- Feign攔截器自動附加
X-User-Id
到請求頭。
- Feign攔截器自動附加
- 微服務B:重復步驟3的流程。
3. 配置管理
我們已經解決了微服務間的通信、注冊發現、路由、登錄鑒權等核心問題,但還有三大痛點懸而未決:
- 網關路由硬編碼:路由規則寫在
application.yml
里,改個路徑就得重啟網關? - 業務配置寫死:數據庫連接、業務開關、限流閾值全在代碼里,調個參數就得重新打包部署?
- 重復配置爆炸:每個微服務都復制粘貼Redis、MySQL、日志配置,一改全改,維護成本飆升?
解決方案:統一配置中心
用Nacos/Apollo等配置中心,把所有配置集中管理,實現:
-
動態路由:網關路由規則放到配置中心,熱更新無需重啟。
# Nacos中動態配置網關路由 spring:cloud:gateway:routes:- id: user-serviceuri: lb://user-servicepredicates:- Path=/user/** # 修改后立即生效
-
業務配置動態化:用
@RefreshScope
注解,運行時刷新配置。@Component @RefreshScope public class DynamicConfig {@Value("${order.timeout:30}") // 配置中心修改后自動更新private Long timeout; }
-
共享配置模板:
- 通用配置(如Redis、MySQL)抽成
shared-config.yml
,所有微服務復用。 - 差異化配置(如端口、服務名)獨立維護,避免冗余。
- 通用配置(如Redis、MySQL)抽成