SpringBoot應用搭建之使用ThreadLocal儲存當前登入資訊
在我們的SpringBoot應用中,通常程式碼都是分層的,介面層、服務層、持久層,大部分的介面請求中我們都需要使用當前登入的使用者資訊,比如使用者名稱、使用者編碼、當前組織等資訊。通常如果是使用session、header、token來儲存當前登入資訊,不管用哪種方式,都需要每一層程式碼傳遞下去,這樣很麻煩,程式碼看起來也不夠簡潔。
我們知道前前端發起的每個請求都會對應一個執行緒,我們只要在這一個執行緒裡定義一個共享變數來儲存當前登入資訊,這樣就可以方便的再任何地方取得當前登入資訊了。java 為我們提供了一個ThreadLocal類,本地執行緒,其實它的物件是本地執行緒的區域性變數。該變數為其所屬執行緒所有,各個執行緒互不影響,我們可以將需要的資料儲存到該物件中。但是要注意執行緒結束要釋放該物件中的資料,不然會出現記憶體洩露的問題。
首先我們定義一個類,該類用於初始化ThreadLocal的物件,同時提供基本的設定資料方法,獲取資料方法,移除資料方法:
package com.xtoad.ecms.common.web;import java.util.HashMap;import java.util.Map;/** * 獲取當前使用者資訊的輔助類 * * @author xtoad * @date 2021/02/19 */public class UserContext { private static ThreadLocal<Map<String, Object>> threadLocal; /** * tokenKey */ public static final String CONTEXT_KEY_USER_TOKEN = "token"; /** * 使用者idKey */ public static final String CONTEXT_KEY_USER_ID = "userId"; /** * 使用者名稱 */ public static final String CONTEXT_KEY_USER_NAME = "userName"; static { threadLocal = new ThreadLocal<>(); } /** * 設定資料 * * @param key 鍵 * @param value 值 */ public static void set(String key, Object value) { Map<String, Object> map = threadLocal.get(); if (map == null) { map = new HashMap<>(6); threadLocal.set(map); } map.put(key, value); } /** * 獲取資料 * * @param key 鍵 * @return 值 */ public static Object get(String key) { Map<String, Object> map = threadLocal.get(); if (map == null) { map = new HashMap<>(6); threadLocal.set(map); } return map.get(key); } /** * 清除資料 * */ public static void remove() { threadLocal.remove(); } public static void setUserId(String userId) { set(CONTEXT_KEY_USER_ID, userId); } public static Long getUserId() { Object value = get(CONTEXT_KEY_USER_ID); return Long.valueOf(String.valueOf(value)); } public static void setUserName(String userName) { set(CONTEXT_KEY_USER_NAME, userName); } public static String getUserName() { Object value = get(CONTEXT_KEY_USER_NAME); return String.valueOf(value); } public static void setToken(String token) { set(CONTEXT_KEY_USER_TOKEN, token); } public static String getToken() { Object value = get(CONTEXT_KEY_USER_TOKEN); return String.valueOf(value); }}
ThreadLocal類支援泛型,我們這裡利用一個map 來儲存資料,也可以是User類等。
然後,我們攔截介面請求,在請求裡透過token、session、header等途徑獲取當前登入資訊,存放到UserContext類中,
package com.xtoad.study.common.web.interceptor;import com.auth0.jwt.interfaces.Claim;import com.xtoad.study.common.utils.TokenUtils;import com.xtoad.study.common.web.UserContext;import com.xtoad.study.common.web.annotation.RequiredToken;import com.xtoad.study.common.web.exception.BusinessException;import com.xtoad.study.common.web.exception.ResultCodeEnum;import org.springframework.http.HttpHeaders;import org.springframework.stereotype.Component;import org.springframework.web.method.HandlerMethod;import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.lang.reflect.Method;import java.util.Map;/** * 介面Token驗證的攔截類 * * @author xtoad * @date 2021/02/18 */@Componentpublic class AuthenticationInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest httpServletRequest , HttpServletResponse httpServletResponse, Object object) { // 如果不是對映到方法直接透過 if (!(object instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) object; Method method = handlerMethod.getMethod(); // 檢查方法上是否有RequiredToken註解 if (method.isAnnotationPresent(RequiredToken.class)) { // 若方法上有,則判斷設定的是否false RequiredToken requiredToken = method.getAnnotation(RequiredToken.class); if (!requiredToken.value()) { // 若設定的required = false 則跳過驗證 return true; } } else if (handlerMethod.getBeanType().isAnnotationPresent(RequiredToken.class)) { // 若方法上沒有註解則判斷該方法的類上是否有RequiredToken註解 // 若類上有,則判斷設定的是否false RequiredToken requiredToken = handlerMethod.getBeanType().getAnnotation(RequiredToken.class); if (!requiredToken.value()) { // 若設定的required = false 則跳過驗證 return true; } } // 從 http 請求頭中取出 token String token = httpServletRequest.getHeader(HttpHeaders.AUTHORIZATION); // 驗證 token boolean verifyToken = TokenUtils.verifyToken(token); if (!verifyToken) { httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN); throw new BusinessException(ResultCodeEnum.EXPIRED_TOKEN); } // 儲存當前登入資訊 Map<String, Claim> claims = TokenUtils.getClaims(token); UserContext.setToken(token); UserContext.setUserId(claims.get(UserContext.CONTEXT_KEY_USER_ID).asString()); UserContext.setLoginNo(claims.get(UserContext.CONTEXT_KEY_LOGIN_NO).asString()); UserContext.setUserName(claims.get(UserContext.CONTEXT_KEY_USER_NAME).asString()); UserContext.setNickName(claims.get(UserContext.CONTEXT_KEY_USER_NICK_NAME).asString()); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // ThreadLocal值清除,防止記憶體洩漏 UserContext.remove(); super.afterCompletion(request, response, handler, ex); }}
我們在攔截器中先進行Token 的驗證,如果驗證成功,就取出Token中的登入資訊,這裡你可以根據你的實際情況呼叫快取、資料庫查詢等方式獲取當前登入資訊,然後儲存到UserContext的ThreadLocal物件中。
然後在其他地方,需要用到登入資訊的地方直接 呼叫即可!
String userName = UserContext.get(UserContext.CONTEXT_KEY_USER_NAME);
截圖強調一下重點: