前言
在開發中,發現一個問題:當其它模組想要獲取使用者資訊時,例如儲存一些業務資料時需要獲取使用者 id,都需要先從請求頭上拿到 token,在呼叫 AuthUtil 工具類去解析,最後拿到使用者資訊。每個模組都這麼寫難免顯得繁瑣,並且我認為 token 這東西,讓認證服務來操作就好了,其它的業務服務就專注於業務處理。所以,我考慮來封裝一個使用者工具類,對外提供獲取使用者資訊的功能,並且不需要傳入 token 來獲取,本文就來分享一下。
工具類封裝
封裝工具類本身並不難,難的是我的想法是,當獲取使用者資訊時,不要把 token 摻和進來,最好是業務程式碼中呼叫一個無參的方法,就獲取到使用者資訊了。
這個地方我在 SpringSecurity 中受到了啓發,在 SpringSecurity 中,有一個元件叫做 SecurityContextHolder,它裡面就封裝了使用者資訊,他的主要做法是在一個請求開始時,將使用者資訊從請求資訊中解析出來,設定到一個ThreadLocal 的變數中,請求結束時,在將當前使用者資訊從 ThreadLocal 中清除,這樣在整個請求流程中,我可以從任意位置呼叫SecurityContextHolder 來獲取使用者資訊,並且多執行緒互不影響,我感覺這個實現方案很好,就自己學習著實現了一下。
首先自定義了一個工具類,裡面就是封裝了一個ThreadLocal 的 String 型別變數,用來封裝使用者 id,同時提供了 set、get 方法,還有一個clear 方法,在這個方法中,呼叫了ThreadLocal 變數的 remove 方法,這是因為ThreadLocal 應用了一個執行緒池,裡面的執行緒是會被複用的,如果使用完畢後不清除,會出現執行緒安全問題。
/** * 自定義的上下文類,使用ThreadLocal來儲存每個執行緒訪問時的userId */ public class UserUtil { private static final ThreadLocal<String> USER_ID = new ThreadLocal<>(); public static void setUserId(String userId) { USER_ID.set(userId); } public static String getUserId() { return USER_ID.get(); } public static void clear() { USER_ID.remove(); } }
工具類封裝好了,現在需要在請求開始時呼叫setUserId 方法,並且在請求結束時呼叫clear 方法,這個操作使用攔截器來做就比較合適。如下,自定義了一個攔截器,在請求開始時儲存使用者資訊,在請求結束時清除使用者資訊。
/** * 攔截器:用於將請求頭中的使用者資訊解析 封裝userId到UserContext中 */ public class UserInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Token"); if (StrUtil.isNotBlank(token)) { String uid = AuthUtil.getLoginUid(token); UserUtil.setUserId(uid); } return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserUtil.clear(); } }
還需要進行一些配置,註冊這個攔截器,程式碼比較簡單,貼一下吧
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new UserInterceptor()); } }
上述程式碼完成後,每個請求開始時,都會在攔截器中 封裝使用者資訊,也會在請求結束時清除。在整個請求的任意位置,都可以呼叫getUserId 方法獲取使用者 id 了。
跨模組共享
上面的程式碼,展示了在一個 SpringBoot 模組中如何配置使用者資訊獲取的工具類,但是如果在微服務專案中,我不想在每個模組中都配置一遍這些內容,所以我考慮在專案的 common 模組封裝,這樣其它模組僅呼叫即可,就算有什麼改動,也只改動一處即可。
首先要做的就是將上面的程式碼,遷移到 common 模組中,然後其它模組中直接呼叫 UserUtil 的 getUserId 方法即可獲取當前登入使用者 id。
還有兩個問題需要處理一下:
第一、 common 工程為 maven 工程,所以標註了@Configuration 註解的類是不會被掃描的,自動掃描@Configuration 是 SpringBoot 提供的自動配置特性。
這裏需要在 common 工廠的 resources 目錄下建立一個檔案: \resources\META-INF\spring\org.springframework.boot.autoconfigure.AutoConfiguration.imports
裡面配置想要被掃描的配置類路徑
com.xb.blog.common.core.config.WebMvcConfig
這樣這個配置類就可以被掃描到了。
第二、common 模組被所有模組依賴,common 爲了實現攔截器必須引入 web 的 starter,但是這和 gateway 的 WebFlux 衝突,gateway 模組並不需要引入 web starter。所以在WebMvcConfig 中還需要新增一處配置
@Configuration @ConditionalOnClass(name = "org.springframework.web.servlet.DispatcherServlet") public class WebMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new UserInterceptor()); } }
使用條件裝配註解@ConditionalOnClass,可實現只有模組中存在DispatcherServlet 才應用這個配置,這也是從 SpringBoot 原始碼中學習到的。
總結
以上,就完成了公共使用者資訊獲取工具的封裝,支援多模組共享,希望可以幫到你。