在微服務架構中,用戶登錄和身份校驗的處理方式確實與單體應用有所不同。在單體架構中,一旦用戶通過身份驗證,其會話信息可以在整個應用范圍內共享,所有模塊都能訪問到用戶信息。然而,在微服務架構下,每個服務獨立部署且通常運行在不同的進程中,因此需要一種機制來確保用戶的身份信息能夠在各個微服務之間安全、高效地傳遞和驗證。
目錄
?
網關實現路由
網關登錄鑒權
鑒權思路
登錄校驗流程圖
網關過濾器詳解
實現網關過濾器
攔截器流程圖
服務信息鑒權
?
網關實現路由
問題:每個微服務都有不同的地址或端口,入口不同,請求不同數據時要訪問不同的入口,需要維護多個入口地址,前端無法調用nacos,無法實時更新服務列表。
解決方案:采用微服務網關,數據在網絡間傳輸,從一個網絡傳輸到另一網絡時就需要經過網關來做數據的路由和轉發。
在微服務中新建一個網關模塊,作為網關微服務
引入依賴(需要加入nacos依賴,讓nacos管理)
<dependencies><!--common--><dependency><groupId>com.heima</groupId><artifactId>hm-common</artifactId><version>1.0.0</version></dependency><!--網關--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId></dependency><!--nacos discovery--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><!--負載均衡--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId></dependency></dependencies>
在application.yaml
文件,配置路由
gateway:routes:- id: item # 路由規則id,自定義,唯一uri: lb://item-service # 路由的目標服務,lb代表負載均衡,會從注冊中心拉取服務列表predicates: # 路由斷言,判斷當前請求是否符合當前規則,符合則路由到目標服務- Path=/items/**,/search/** # 這里是以請求路徑作為判斷規則
經測試成功
網關登錄鑒權
問題:單體架構時我們只需要完成一次用戶登錄、身份校驗,就可以在所有業務中獲取到用戶信息。但是,在微服務中,每個微服務都獨立部署,一般只有用戶微服務能校驗登錄信息,事實上,這是不安全的。
解決方案:既然網關是所有微服務的入口,一切請求都需要先經過網關。我們完全可以把登錄校驗的工作放到網關去做。
鑒權思路
登錄是基于JWT來實現的,校驗JWT的算法復雜,而且需要用到秘鑰。我們不可能讓個微服務都需要知道JWT的秘鑰,不安全。也不可能每個微服務重復編寫登錄校驗代碼、權限校驗代碼,麻煩。
所以,我們在網關和用戶服務保存秘鑰,開發登錄校驗功能。
登錄校驗流程圖
我們可以看到:前端——網關——后端服務
我們在網關層去實現過濾請求。
網關過濾器詳解
Gateway
內部工作的基本原理:
如圖所示:
-
客戶端請求進入網關后由
HandlerMapping
對請求做判斷,找到與當前請求匹配的路由規則(Route
),然后將請求交給WebHandler
去處理 -
WebHandler
則會加載當前路由下需要執行的過濾器鏈(Filter chain
),然后按照順序逐一執行過濾器(后面稱為Filter
) -
圖中
Filter
被虛線分為左右兩部分,是因為Filter
內部的邏輯分為pre
和post
兩部分,分別會在請求路由到微服務之前和之后被執行 -
只有所有
Filter
的pre
邏輯都依次順序執行通過后,請求才會被路由到微服務 -
微服務返回結果后,再倒序執行
Filter
的post
邏輯 -
最終把響應結果返回
反正就是:我們需要在NettyRoutingFilter過濾器之前,在發起Request時,即pre時,定義一個過濾器,進行網關登錄校驗。
實現網關過濾器
我們采用全局過濾器,作用范圍是所有路由,即GlobalFilter。
package com.hmall.gateway.filters;import com.hmall.common.exception.UnauthorizedException;
import com.hmall.gateway.config.AuthProperties;
import com.hmall.gateway.utils.JwtTool;
import lombok.RequiredArgsConstructor;
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.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;import java.util.List;@RequiredArgsConstructor
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {private final AuthProperties authProperties;private final JwtTool jwtTool;private final AntPathMatcher antPathMatcher = new AntPathMatcher();/*** 過濾器* @param exchange* @param chain* @return*/@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 1.獲取請求對象ServerHttpRequest 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 (headers != null && !headers.isEmpty()) {token = headers.get(0);}// 4.解析tokenLong userId = null;try {userId = jwtTool.parseToken(token);} catch (UnauthorizedException e) {// 如果無效,攔截ServerHttpResponse response = exchange.getResponse();response.setStatusCode(HttpStatus.UNAUTHORIZED);return response.setComplete();}// 5.獲取用戶信息String userInfo = userId.toString();ServerWebExchange swe = exchange.mutate().request(builder -> builder.header("user-info",userInfo )).build();// 6.放行return chain.filter(swe);}/*** 判斷路徑是否需要攔截* @param antPath* @return*/private boolean isExclude(String antPath) {for (String pathPattern : authProperties.getExcludePaths()) {if(antPathMatcher.match(pathPattern, antPath)){return true;}}return false;}// 優先級@Overridepublic int getOrder() {return 0;}
}
因為我采用了JWT令牌進行校驗,所以引入了JWT工具類,進行令牌的獲取與解析。
至于AuthProperties是配置了不需要鑒權就能訪問的路徑。
經測試,網關已經可以完成登錄校驗并獲取登錄用戶身份信息。
問題:當網關將請求轉發到微服務時,微服務如何獲取用戶身份,我們不可能每個微服務都寫一個攔截器去得到用戶身份信息。
解決方案:將用戶信息以請求頭的方式傳遞到下游微服務。然后微服務可以從請求頭中獲取登錄用戶信息(上述代碼第五步已經完成了)。考慮到微服務內部可能很多地方都需要用到登錄用戶信息,因此我們可以利用SpringMVC的攔截器來實現登錄用戶信息獲取,并存入ThreadLocal。
攔截器流程圖
提供一個用于保存登錄用戶的ThreadLocal工具UserContext:
public class UserContext {private static final ThreadLocal<Long> tl = new ThreadLocal<>();/*** 保存當前登錄用戶信息到ThreadLocal* @param userId 用戶id*/public static void setUser(Long userId) {tl.set(userId);}/*** 獲取當前登錄用戶信息* @return 用戶id*/public static Long getUser() {return tl.get();}/*** 移除當前登錄用戶信息*/public static void removeUser(){tl.remove();}
}
在公用模塊下定義一個攔截器:
public class UserInfoInterceptor implements HandlerInterceptor {/*** 請求攔截器* @param request* @param response* @param handler* @return* @throws Exception*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.獲取請求頭中的 用戶信息String userInfo = request.getHeader("user-info");// 2.判斷是否為空if (StringUtils.isNotBlank(userInfo)) {UserContext.setUser(Long.valueOf(userInfo));}// 3.放行return true;}/*** 響應攔截器* @param request* @param response* @param handler* @param ex* @throws Exception*/@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {UserContext.removeUser();}
}
編寫SpringMVC
的配置類,配置登錄攔截器:
@Configuration
@ConditionalOnClass(DispatcherServlet.class)
public class MvcConfig implements WebMvcConfigurer {public void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new UserInfoInterceptor());}
}
注意:這個配置類默認是不會生效的,基于SpringBoot的自動裝配原理,我們要將其添加到resources
目錄下的META-INF/spring.factories
文件中:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.hmall.common.config.MyBatisConfig,\com.hmall.common.config.MvcConfig
經測試,服務得到網關傳遞的用戶身份信息。
服務信息鑒權
問題:因為之前編寫的過濾器和攔截器功能,微服務可以輕松獲取登錄用戶信息。但是,有時候請求到達微服務后還需要調用其它多個微服務。我們沒有實現服務之間的用戶身份信息的傳遞。
解決方案:由于微服務獲取用戶信息是通過攔截器在請求頭中讀取,因此要想實現微服務之間的用戶信息傳遞,就必須在微服務發起調用時把用戶信息存入請求頭。
因為我們之前微服務之間調用是基于OpenFeign來實現的,并不是我們自己發送的請求。所以我們可以采用Feign中提供的一個攔截器接口:feign.RequestInterceptor。
在公用模塊下定義一個userInfoRequestInterceptor
/*** feign請求攔截器 微服務之間的遠程調用時,將當前登錄用戶的userId傳遞給目標服務* @return*/@Beanpublic RequestInterceptor userInfoRequestInterceptor() {return new RequestInterceptor() {public void apply(RequestTemplate requestTemplate) {Long userId = UserContext.getUser();if (userId != null) {requestTemplate.header("user-info", userId.toString());}}};}
總結:
- 為了實現網關處簡便的登錄校驗,我們采用了GlobalFilter;
- 為了實現網關傳遞用戶信息到多個微服務,我們采用了UserInfoInterceptor ;
- 為了實現微服務之間用戶身份信息傳遞,我們采用了userInfoRequestInterceptor。
?