使用執行緒本地變數儲存會員資訊

鱼摆摆不摆發表於2024-09-03

執行緒本地變數

功能:JWT單點登入時,後端透過前端請求header中的token解析出會員資訊,這樣就可以不透過前端傳遞就可以獲取當前會員的資訊。

執行緒本地變數
import com.guaigen.train.common.resp.MemberLoginResp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LoginMemberContext {
    private static final Logger LOG = LoggerFactory.getLogger(LoginMemberContext.class);

    private static ThreadLocal<MemberLoginResp> member = new ThreadLocal<>();

    public static MemberLoginResp getMember() {
        return member.get();
    }

    public static void setMember(MemberLoginResp member) {
        LoginMemberContext.member.set(member);
    }

    public static Long getId() {
        try {
            return member.get().getId();
        } catch (Exception e) {
            LOG.info("獲取會員登入資訊異常:{}", e);
            throw e;
        }
    }
}

定義了一個名為 LoginMemberContext 的工具類,該類使用了 ThreadLocal 來儲存和獲取當前會員的登入資訊(MemberLoginResp)。ThreadLocal 確保每個執行緒都有自己獨立的 MemberLoginResp 例項,這在多執行緒環境中非常有用,例如在 Web 應用程式中,每個請求由單獨的執行緒處理。

程式碼的主要功能如下:

  1. 獲取當前會員資訊

    • getMember() 方法用於獲取當前執行緒中儲存的會員登入資訊。
  2. 設定當前會員資訊

    • setMember(MemberLoginResp member) 方法用於將會員資訊儲存到當前執行緒中。
  3. 獲取會員ID

    • getId() 方法用於獲取當前執行緒中儲存的會員ID。該方法包含了異常處理,如果在獲取會員ID時發生異常,將記錄錯誤資訊並丟擲異常。

這種設計確保了每個執行緒的登入上下文是獨立的,不會相互影響,適合在需要執行緒隔離的場景中使用。

攔截器攔截執行緒本地變數

攔截器
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.guaigen.train.common.util.JwtUtil;
import com.guaigen.train.common.context.LoginMemberContext;
import com.guaigen.train.common.resp.MemberLoginResp;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

/**
 * 攔截器:Spring框架特有的,常用於登入校驗,許可權校驗,請求日誌列印
 */
@Component
public class MemberInterceptor implements HandlerInterceptor {

    private static final Logger LOG = LoggerFactory.getLogger(MemberInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //獲取header的token引數
        String token = request.getHeader("token");
        if (StrUtil.isNotBlank(token)) {
            LOG.info("獲取會員登入token:{}", token);
            JSONObject loginMember = JwtUtil.getJSONObject(token);
            LOG.info("當前登入會員:{}", loginMember);
            MemberLoginResp member = JSONUtil.toBean(loginMember, MemberLoginResp.class);
            LoginMemberContext.setMember(member);
        }
        return true;
    }
}
基於 Spring 框架的攔截器 (`Interceptor`),名為,用於在每次 HTTP 請求處理之前執行一些操作,主要功能是驗證使用者的登入狀態,並將登入資訊儲存到當前執行緒的上下文中。

主要功能:

  1. 獲取 Token

    • 在每次請求處理之前,攔截器會從請求的 header 中獲取 token 引數。
    • StrUtil.isNotBlank(token) 用於檢查 token 是否非空且非空白字元。
  2. 解析 Token

    • 如果 token 存在且有效,攔截器使用 JwtUtil.getJSONObject(token) 方法將 token 解析為 JSONObject 物件。
    • 日誌記錄:攔截器會將獲取到的 token 以及解析後的會員資訊記錄到日誌中,便於跟蹤和除錯。
  3. 儲存會員登入資訊

    • 解析後的 JSONObject 物件會被轉換為 MemberLoginResp 物件。
    • 呼叫 LoginMemberContext.setMember(member) 方法,將該會員資訊儲存到當前執行緒的上下文中,以便在後續請求處理中使用。
  4. 繼續處理請求

    • 如果攔截器成功獲取並處理了 token,會返回 true,表示可以繼續處理該請求。否則,請求可能會被阻止或進一步處理。

    這個攔截器通常用於登入驗證,確保請求中包含有效的 token,並將登入資訊載入到上下文中供後續使用。這樣的設計在需要使用者認證和許可權管理的場景中非常常見。

可能的擴充套件:

  • 許可權校驗:除了登入驗證,還可以擴充套件該攔截器,新增許可權校驗功能,確保使用者有權訪問某些資源或執行特定操作。
  • 日誌記錄:記錄更多的請求細節,如請求路徑、請求引數等,以便進行審計和問題排查。

配置檔案

config
import com.guaigen.train.common.interceptor.MemberInterceptor;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {
    @Resource
    MemberInterceptor memberInterceptor;

    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(memberInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns(
                        "/member/hello",
                        "/member/member/sendCode",
                        "/member/member/login"
                );
    }
}
Spring MVC 配置類,用於註冊並配置攔截器 `MemberInterceptor`。

程式碼解析:

  1. @Configuration 註解

    • 這個類被標註為一個配置類,意味著它定義了一些 Spring 容器的配置資訊。
  2. WebMvcConfigurer 介面

    • SpringMvcConfig 實現了 WebMvcConfigurer 介面,這是 Spring MVC 提供的一個擴充套件點,允許你自定義配置 MVC 的行為,而不需要繼承 WebMvcConfigurerAdapter(在 Spring 5 中已廢棄)。
  3. @Resource 註解

    • @Resource 註解用於自動注入 MemberInterceptor 例項。Spring 將自動掃描並注入已定義的 MemberInterceptor Bean。
  4. addInterceptors 方法

    • addInterceptors(InterceptorRegistry registry) 方法用於註冊攔截器,並配置它應用於哪些請求路徑。
  5. 配置攔截器路徑

    • registry.addInterceptor(memberInterceptor)MemberInterceptor 新增到攔截器鏈中。
    • .addPathPatterns("/**") 指定攔截器應用於所有路徑(/** 表示匹配所有路徑)。
    • .excludePathPatterns("/member/hello", "/member/member/sendCode", "/member/member/login") 指定某些路徑不被攔截器攔截,這通常是登入、驗證碼傳送等公共介面。

功能概述:

  • 全域性攔截

    • 這個配置類將 MemberInterceptor 應用於所有請求路徑,但排除了一些指定的公共路徑。這意味著除了登入和傳送驗證碼的路徑外,所有其他路徑都將經過 MemberInterceptor 的處理。
  • 典型場景

    • 這種配置方式通常用於保護應用的業務邏輯部分,確保只有透過認證的使用者才能訪問特定資源。同時,也保留了一些不需要認證的公共介面供外部使用。

可擴充套件性:

  • 根據需要,增加或減少 excludePathPatterns 中的路徑,以控制哪些請求路徑不需要經過攔截器。
  • 如果有多個攔截器,也可以透過 registry.addInterceptor(...) 繼續新增和配置其他攔截器。

後端使用

後端Service呼叫
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateTime;
import com.guaigen.train.common.context.LoginMemberContext;
import com.guaigen.train.common.util.SnowUtil;
import com.guaigen.train.member.domain.Passenger;
import com.guaigen.train.member.mapper.PassengerMapper;
import com.guaigen.train.member.req.PassengerSaveReq;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;

@Service
public class PassengerService {

    @Resource
    private PassengerMapper passengerMapper;

    public void save(PassengerSaveReq req) {
        DateTime now = DateTime.now();
        Passenger passenger = BeanUtil.copyProperties(req, Passenger.class);
        passenger.setMemberId(LoginMemberContext.getId());
        passenger.setId(SnowUtil.getSnowflakeNestId());
        passenger.setCreatedTime(now);
        passenger.setModifiedTime(now);
        passengerMapper.insert(passenger);
    }
}

其中passenger.setMemberId(LoginMemberContext.getId());就是透過執行緒本地變數獲取memberId會員資訊

相關文章