切換語言為:簡體

SpringCloud 微服務實現統一封裝使用者資訊,程式碼任意位置隨意獲取

  • 爱糖宝
  • 2024-08-21
  • 2047
  • 0
  • 0

前言

在開發中,發現一個問題:當其它模組想要獲取使用者資訊時,例如儲存一些業務資料時需要獲取使用者 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 原始碼中學習到的。

總結

以上,就完成了公共使用者資訊獲取工具的封裝,支援多模組共享,希望可以幫到你。

0則評論

您的電子郵件等資訊不會被公開,以下所有項目均必填

OK! You can skip this field.