文章目錄
- 前言
- 一、網關路由
- 二、SpringCloudGateway
- 1. 路由過濾
- 2. 網關登錄校驗
- 2.1 鑒權
- 2.2 網關過濾器
- 2.3 登錄校驗
- 2.3.1 JWT
- 2.3.2 登錄校驗過濾器
- 3. 微服務從網關獲取用戶
- 4. 微服務之間用戶信息傳遞
- 三、nacos配置管理
- 問題引入
- 3.1 配置共享
- 3.1.1 在Nacos中添加共享配置
- 3.1.2 拉取共享配置
- 3.2 配置熱更新
- 問題引入
- 實現步驟
- 3.3 動態路由
- 問題引入
- 3.3.1 監聽nacos配置變更,更新路由表
- 3.3.2 實現動態路由
前言
前端請求不能直接訪問微服務,而是要請求網關(SpringCloudGateway)
網關干什么?路由過濾,登錄校驗。
nacos既是注冊中心也可用作配置管理,用來解決各個微服務塊配置文件中相同的配置冗余,配置熱更新屬性,動態路由等。
一、網關路由
數據在網絡間傳輸,從一個網絡傳輸到另一網絡時就需要經過網關來做數據的路由和轉發以及數據安全的校驗。
微服務中的網關作用:前端請求不能直接訪問微服務,而是要請求網關。
- 網關可以做安全控制,也就是登錄身份校驗,校驗通過才放行
- 通過認證后,網關再根據請求判斷應該訪問哪個微服務,將請求轉發過去
二、SpringCloudGateway
網關本身也是一個獨立的微服務,所以也需要單獨建立一個模塊。實現步驟:
- 創建網關微服務模塊
- 引入SpringCloudGateway、NacosDiscovery依賴
- 編寫啟動類
- 配置網關路由
1. 路由過濾
spring:cloud:gateway:routes:- id: itemuri: lb://item-servicepredicates:- Path=/items/**,/search/** #請求路徑必須符合指定規則
RouteDefinition就是具體的路由規則定義。
- id:路由的唯一標示
- predicates:路由斷言,其實就是匹配條件
- filters:路由過濾條件
- uri:路由目標地址,lb://代表負載均衡,從注冊中心獲取目標微服務的實例列表,并且負載均衡選擇一個訪問。
2. 網關登錄校驗
2.1 鑒權
在網關中保存密鑰開發登錄校驗功能
問題
1、怎么在轉發之前做登錄校驗
2、校驗JWT之后,怎么把用戶信息傳遞給微服務
3、微服務之間怎么傳遞用戶信息
2.2 網關過濾器
實現原理
- 客戶端請求進入網關后由HandlerMapping對請求做判斷,找到與當前請求匹配的路由規則(Route),然后將請求交給WebHandler去處理。
- WebHandler則會加載當前路由下需要執行的過濾器鏈(Filter chain),然后按照順序逐一執行過濾器(后面稱為Filter)。
- 圖中Filter被虛線分為左右兩部分,是因為Filter內部的邏輯分為pre和post兩部分,分別會在請求路由到微服務之前和之后被執行。
- 只有所有Filter的pre邏輯都依次順序執行通過后,請求才會被路由到微服務。
- 微服務返回結果后,再倒序執行Filter的post邏輯。
- 最終把響應結果返回。
定義一個過濾器,在其中實現登錄校驗邏輯,并且將過濾器執行順序定義到NettyRoutingFilter之前?
網關過濾器
- GatewayFilter:路由過濾器,作用范圍比較靈活,可以是任意指定的路由Route.
- GlobalFilter:全局過濾器,作用范圍是所有路由,不可配置。FilteringWebHandler在處理請求時,會將GlobalFilter裝飾為GatewayFilter,然后放到同一個過濾器鏈中,排序以后依次執行。
- AddRequestHeaderGatewayFilterFacotry: 就是添加請求頭的過濾器,可以給請求添加一個請求頭并傳遞到下游微服務。直接在yaml文件中配置
1、自定義GatewayFilter
定義一個類,這個類的名稱一定要以GatewayFilterFactory為后綴,這個類繼承了AbstractGatewayFilterFactory
2、自定義GlobalFilter
定義一個類,直接實現GlobalFilter接口。
2.3 登錄校驗
2.3.1 JWT
- AuthProperties:配置登錄校驗需要攔截的路徑,因為不是所有的路徑都需要登錄才能訪問
- JwtProperties:定義與JWT工具有關的屬性,比如秘鑰文件位置
- SecurityConfig:工具的自動裝配
- JwtTool:JWT工具,其中包含了校驗和解析token的功能
- hmall.jks:秘鑰文件
2.3.2 登錄校驗過濾器
定義一個登錄校驗的過濾器,實現GlobalFilter接口。
@Component
@RequiredArgsConstructor
@EnableConfigurationProperties(AuthProperties.class)
public class AuthGlobalFilter implements GlobalFilter, Ordered {private final JwtTool jwtTool;private final AuthProperties authProperties;private final AntPathMatcher antPathMatcher = new AntPathMatcher();@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 1.獲取RequestServerHttpRequest request = exchange.getRequest();// 2.判斷是否不需要攔截if(isExclude(request.getPath().toString())){// 無需攔截,直接放行return chain.filter(exchange);}// 3.獲取請求頭中的tokenString token = null;List<String> headers = request.getHeaders().get("authorization");if (!CollUtils.isEmpty(headers)) {token = headers.get(0);}// 4.校驗并解析tokenLong userId = null;try {userId = jwtTool.parseToken(token);} catch (UnauthorizedException e) {// 如果無效,攔截ServerHttpResponse response = exchange.getResponse();response.setRawStatusCode(401);return response.setComplete();}// TODO 5.如果有效,傳遞用戶信息//保存用戶請求頭String userInfo = userId.toString();ServerWebExchange ex = exchange.mutate().request(b -> b.header("user-info",userInfo)).build();// 6.放行return chain.filter(exchange);}private boolean isExclude(String antPath) {for (String pathPattern : authProperties.getExcludePaths()) {if(antPathMatcher.match(pathPattern, antPath)){return true;}}return false;}@Overridepublic int getOrder() {return 0;}
}
3. 微服務從網關獲取用戶
將用戶信息以請求頭的方式傳遞到下游微服務,然后微服務可以從請求頭中獲取登錄用戶信息。考慮到微服務內部可能很多地方都需要用到登錄用戶信息,因此我們可以利用SpringMVC的攔截器來實現登錄用戶信息獲取,并存入ThreadLocal,方便后續使用。
實現步驟:
- 改造網關過濾器,在獲取用戶信息后保存到請求頭,轉發到下游微服務
- 編寫微服務攔截器,攔截請求獲取用戶信息,保存到ThreadLocal后放行(因為每個微服務都執行獲取用戶信息的邏輯,因此統一攔截所有請求,進行處理(AOP的思想))
4. 微服務之間用戶信息傳遞
由于微服務獲取用戶信息是通過攔截器在請求頭中讀取,因此要想實現微服務之間的用戶信息傳遞,就必須在微服務發起調用時把用戶信息存入請求頭。
微服務之間調用是基于OpenFeign來實現的,并不是我們自己發送的請求。我們如何才能讓每一個由OpenFeign發起的請求自動攜帶登錄用戶信息呢?
Feign中提供的一個攔截器接口:feign.RequestInterceptor
實現方式
定義一個類,實現RequestInterceptor接口,實現接口中的apply方法,用RequestTemplate類來添加請求頭,將用戶信息保存到請求頭中。這樣以來,每次OpenFeign發起請求的時候都會調用該方法,傳遞用戶信息。
@Bean
public RequestInterceptor userInfoRequestInterceptor(){return new RequestInterceptor() {@Overridepublic void apply(RequestTemplate template) {// 獲取登錄用戶Long userId = UserContext.getUser();if(userId == null) {// 如果為空則直接跳過return;}// 如果不為空則放入請求頭中,傳遞給下游微服務template.header("user-info", userId.toString());}};
}
三、nacos配置管理
問題引入
- 網關路由在配置文件中寫死了,如果變更必須重啟微服務
- 某些業務配置在配置文件中寫死了,每次修改都要重啟服務
- 每個微服務都有很多重復的配置,維護成本高
nacos: 無需重啟即可生效,實現配置熱更新。實現動態路由功能,無需重啟網關即可修改路由配置。
3.1 配置共享
3.1.1 在Nacos中添加共享配置
抽取配置文件yaml中重復的模塊,如jdbc相關配置(datasource、mybatis-plus)、日志配置(logging)、swagger(knife4j)以及OpenFeign的配置(feign)
3.1.2 拉取共享配置
如何去加載nacos中的配置文件?
實現步驟
1、在模塊中引入依賴
<!--nacos配置管理--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId></dependency><!--讀取bootstrap文件--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-bootstrap</artifactId></dependency>
2、新建bootstrap.yaml,配置nacos地址
spring:cloud:nacos:server-addr: 192.168.150.101 # nacos地址config:file-extension: yaml # 文件后綴名shared-configs: # 共享配置- dataId: shared-jdbc.yaml # 共享mybatis配置- dataId: shared-log.yaml # 共享日志配置- dataId: shared-swagger.yaml # 共享日志配置
3.2 配置熱更新
問題引入
業務相關參數,將來可能會根據實際情況臨時調整。例如購物車業務,購物車數量有一個上限,默認是10。
實現步驟
1、在nacos中添加配置屬性
2、新建一個屬性讀取類,在需要使用該屬性的業務類中注入。
3.3 動態路由
問題引入
網關的路由配置全部是在項目啟動的時候加載,并且一經加載就會緩存到內存中的路由表內(一個Map),不會改變,也不會監聽路由變更。
- 如何監聽Nacos配置變更?
- 如何把路由信息更新到路由表?
3.3.1 監聽nacos配置變更,更新路由表
使用 Nacos 動態監聽配置接口來實現。
public void addListener(String dataId, String group, Listener listener)
核心點:
- 創建ConfigService,連接到Nacos(spring-cloud-starter-alibaba-nacos-config自動裝配,拿到NacosConfigManager就等于拿到了ConfigService)
- 添加配置監聽器,編寫配置變更的通知處理邏輯
String serverAddr = "{serverAddr}";
String dataId = "{dataId}";
String group = "{group}";
// 1.創建ConfigService,連接Nacos
Properties properties = new Properties();
properties.put("serverAddr", serverAddr);
ConfigService configService = NacosFactory.createConfigService(properties);
// 2.讀取配置
String content = configService.getConfig(dataId, group, 5000);
// 3.添加配置監聽器
configService.addListener(dataId, group, new Listener() {@Overridepublic void receiveConfigInfo(String configInfo) {// 配置變更的通知處理System.out.println("recieve1:" + configInfo);}@Overridepublic Executor getExecutor() {return null;}
});
更新路由表
package org.springframework.cloud.gateway.route;
import reactor.core.publisher.Mono;
/*** @author Spencer Gibb*/
public interface RouteDefinitionWriter {/*** 更新路由到路由表,如果路由id重復,則會覆蓋舊的路由*/Mono<Void> save(Mono<RouteDefinition> route);/*** 根據路由id刪除某個路由*/Mono<Void> delete(Mono<String> routeId);
}
3.3.2 實現動態路由
1、網關gateway引入依賴
<!--統一配置管理-->
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--加載bootstrap-->
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
2、在網關gateway的resources目錄創建bootstrap.yaml文件
spring:application:name: gatewaycloud:nacos:server-addr: 192.168.150.101config:file-extension: yamlshared-configs:- dataId: shared-log.yaml # 共享日志配置
3、在gateway中定義配置監聽器
package com.hmall.gateway.route;import cn.hutool.json.JSONUtil;
import com.alibaba.cloud.nacos.NacosConfigManager;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import com.hmall.common.utils.CollUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;import javax.annotation.PostConstruct;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;@Slf4j
@Component
@RequiredArgsConstructor
public class DynamicRouteLoader {private final RouteDefinitionWriter writer;private final NacosConfigManager nacosConfigManager;// 路由配置文件的id和分組private final String dataId = "gateway-routes.json";private final String group = "DEFAULT_GROUP";// 保存更新過的路由idprivate final Set<String> routeIds = new HashSet<>();@PostConstructpublic void initRouteConfigListener() throws NacosException {// 1.注冊監聽器并首次拉取配置String configInfo = nacosConfigManager.getConfigService().getConfigAndSignListener(dataId, group, 5000, new Listener() {@Overridepublic Executor getExecutor() {return null;}@Overridepublic void receiveConfigInfo(String configInfo) {updateConfigInfo(configInfo);}});// 2.首次啟動時,更新一次配置updateConfigInfo(configInfo);}private void updateConfigInfo(String configInfo) {log.debug("監聽到路由配置變更,{}", configInfo);// 1.反序列化List<RouteDefinition> routeDefinitions = JSONUtil.toList(configInfo, RouteDefinition.class);// 2.更新前先清空舊路由// 2.1.清除舊路由for (String routeId : routeIds) {writer.delete(Mono.just(routeId)).subscribe();}routeIds.clear();// 2.2.判斷是否有新的路由要更新if (CollUtils.isEmpty(routeDefinitions)) {// 無新路由配置,直接結束return;}// 3.更新路由routeDefinitions.forEach(routeDefinition -> {// 3.1.更新路由writer.save(Mono.just(routeDefinition)).subscribe();// 3.2.記錄路由id,方便將來刪除routeIds.add(routeDefinition.getId());});}
}
4、在nacos中配置路由信息,路由文件名為gateway-routes.json,類型為json
[{"id": "item","predicates": [{"name": "Path","args": {"_genkey_0":"/items/**", "_genkey_1":"/search/**"}}],"filters": [],"uri": "lb://item-service"},....