java接口用戶上下文的設(shè)計(jì)與實(shí)現(xiàn)
前言
作為一名從業(yè)已達(dá)六年的老碼農(nóng),我的工作主要是開發(fā)后端Java業(yè)務(wù)系統(tǒng),包括各種管理后臺(tái)和小程序等。在這些項(xiàng)目中,我設(shè)計(jì)過單/多租戶體系系統(tǒng),對接過許多開放平臺(tái),也搞過消息中心這類較為復(fù)雜的應(yīng)用,但幸運(yùn)的是,我至今還沒有遇到過線上系統(tǒng)由于代碼崩潰導(dǎo)致資損的情況。這其中的原因有三點(diǎn):一是業(yè)務(wù)系統(tǒng)本身并不復(fù)雜;二是我一直遵循某大廠代碼規(guī)約,在開發(fā)過程中盡可能按規(guī)約編寫代碼;三是經(jīng)過多年的開發(fā)經(jīng)驗(yàn)積累,我成為了一名熟練工,掌握了一些實(shí)用的技巧。
考慮到文字太過寡淡,我先上一張圖
在Spring Boot中,默認(rèn)情況下,每個(gè)請求到達(dá)時(shí)都會(huì)分配一個(gè)單獨(dú)的線程來處理,而且請求的發(fā)起人也不一定都是同一個(gè)人,所以一個(gè)請求對應(yīng)一個(gè)用戶上下文,并且要求線程隔離
,即不同線程的用戶上下文互不影響,最后用戶上下文還需要隨著線程的結(jié)束而刪除。
本文我會(huì)從用戶上下文如何構(gòu)建、如何使用、如何刪除這三個(gè)方面解釋接口用戶上下文的設(shè)計(jì)與實(shí)現(xiàn)。
本文參考項(xiàng)目源碼地址:summo-springboot-interface-demo
一、接口用戶上下文的構(gòu)建、使用、清除
1. 利用Filter攔截到每一個(gè)請求
由于接口散落在各個(gè)Controller中,且絕大部分接口都是需要這個(gè)用戶上下文的(注:也不排除不需要用戶上下文的接口存在),所以這里需要統(tǒng)一入口進(jìn)行創(chuàng)建、銷毀。看起來可以使用AOP的方式來實(shí)現(xiàn),
不過這里有一個(gè)更合適的方案,利用SpringBoot自帶的Filter【javax.servlet.Filter】來實(shí)現(xiàn)。
實(shí)現(xiàn)起來非常簡單,我這邊自定義了一個(gè)WebFilter,代碼如下:
WebFilter.java
package com.summo.filter; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.summo.context.GlobalUserContext; import com.summo.context.UserContext; import lombok.extern.slf4j.Slf4j; import org.slf4j.MDC; import org.springframework.stereotype.Component; @Slf4j @Component public class WebFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { Filter.super.init(filterConfig); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { try { //獲取本次接口的唯一碼 String token = java.util.UUID.randomUUID().toString().replaceAll("-", "").toUpperCase(); MDC.put("requestId", token); //獲取請求頭 HttpServletRequest httpServletRequest = (HttpServletRequest)servletRequest; HttpServletResponse httpServletResponse = (HttpServletResponse)servletResponse; log.info("當(dāng)前請求鏈接為:[{}]", httpServletRequest.getRequestURL()); //設(shè)置用戶上下文 UserContext userContext = new UserContext(); userContext.setUserId(1L); GlobalUserContext.setUserContext(userContext); //執(zhí)行doFilter,這行一定要加,否則程序會(huì)中斷掉 filterChain.doFilter(httpServletRequest, httpServletResponse); } catch (Exception e) { log.error("do doFilter exception", e); } finally { GlobalUserContext.clear(); MDC.remove("requestId"); } } @Override public void destroy() { Filter.super.destroy(); } }
這段代碼的核心方法是:public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
我們可以在這個(gè)方法里面獲取到ServletRequest和ServletResponse,這兩個(gè)類能獲取到代表著我們可以操作整個(gè)請求過程,這里如何確定當(dāng)前請求的用戶?下面有一張流程圖供大家參考:
還有一種做法是使用JWT來當(dāng)做用戶token,因?yàn)镴WT本身就可以存儲(chǔ)一些信息,所以我們就不需要去緩存用戶信息了,直接解析JWT即可,這種做法在分布式應(yīng)用中很常見。
2. 獲取當(dāng)前請求的線程
上面已經(jīng)獲取到用戶信息了,現(xiàn)在需要將用戶信息放入用戶上下文中,但由于請求的發(fā)起人不一定都是同一個(gè)人,所以一個(gè)請求對應(yīng)著一個(gè)用戶上下文,也即一個(gè)線程設(shè)置一個(gè)上下文。那么這里就需要獲取到當(dāng)前線程才能設(shè)置上下文。
獲取當(dāng)前線程有很多辦法,這里推薦使用阿里巴巴開源的TTL框架(TransmittableThreadLocal)
來實(shí)現(xiàn),功能強(qiáng)大且用法簡單。
引入方法如下:
<dependency> <groupId>com.alibaba</groupId> <artifactId>transmittable-thread-local</artifactId> <version>2.11.1</version> </dependency>
使用方法如下:
private static final TransmittableThreadLocal<UserContext> USER_HOLDER = new TransmittableThreadLocal<>();
直接new一個(gè)對象就行,而且支持泛型。
3. 用戶上下文生命周期管理
對于用戶上下文的生命周期管理需要定義3個(gè)方法:
- 設(shè)置上下文用戶信息;
- 獲取上下文用戶信息
清除上下文用戶信息
以上方法均為靜態(tài)方法。
下面是一個(gè)簡單的例子:
GlobalUserContext.java
package com.summo.context; import com.alibaba.ttl.TransmittableThreadLocal; public class GlobalUserContext { private static final TransmittableThreadLocal<UserContext> USER_HOLDER = new TransmittableThreadLocal<>(); /** * 設(shè)置上下文用戶信息 * * @param user 用戶信息 */ public static void setUserContext(UserContext user) { USER_HOLDER.set(user); } /** * 獲取上下文用戶信息 */ public static UserContext getUserContext() { return USER_HOLDER.get(); } /** * 清除上下文用戶信息 */ public static void clear() { USER_HOLDER.remove(); } }
UserContext.java
package com.summo.context; import lombok.Data; @Data public class UserContext { /** * 用戶ID */ private Long userId; }
調(diào)用方式如下:
設(shè)置上下文用戶信息:GlobalUserContext.setUserContext(userContext);
獲取上下文用戶信息:GlobalUserContext.getUserContext();
清除上下文用戶信息:GlobalUserContext.clear();
4. 用戶上下文的使用
獲取用戶上下文很方便,調(diào)用GlobalUserContext.getUserContext();
就行了,這里我主要講一下用戶上下文的使用場景。
a. 身份認(rèn)證
可以將用戶的身份認(rèn)證信息(如用戶名、密碼、權(quán)限等)保存在用戶上下文中,在需要進(jìn)行鑒權(quán)的地方進(jìn)行驗(yàn)證。
b. 用戶日志記錄
正如《優(yōu)化接口設(shè)計(jì)的思路》系列:第三篇—在用戶使用系統(tǒng)過程中留下痕跡 的方法三.
c. 防止接口數(shù)據(jù)越權(quán)
舉個(gè)例子,比如有些業(yè)務(wù)需要獲取當(dāng)前登錄用戶的信息、當(dāng)前登錄用戶的收藏、當(dāng)前登錄用戶的瀏覽記錄,這樣的接口總不能在接口上傳一個(gè)userId吧?真要這樣干了,非得給安全罵死。。。
利用用戶上下文的話,接口就可以不用傳遞任何參數(shù)獲取到當(dāng)前用戶的userId,實(shí)現(xiàn)你的需求啦。
d. 跨服務(wù)調(diào)用
在分布式系統(tǒng)中,可以將用戶上下文信息傳遞給其他服務(wù),以保持用戶的一致性和連貫性。
e. 監(jiān)控和統(tǒng)計(jì)
可以將用戶上下文中的信息用于系統(tǒng)的監(jiān)控和統(tǒng)計(jì),如請求的處理時(shí)間、請求的次數(shù)等。
5. 用戶上下文的刪除
刪除很簡單,調(diào)用GlobalUserContext.clear();
即可,詳情可見WebFilter.java內(nèi)容。
二. 用戶登錄&認(rèn)證
上面主要是說怎么獲取到接口請求的用戶以及怎么設(shè)置用戶上下文,但沒說用戶身份是什么時(shí)候確認(rèn)的以及怎么確認(rèn)的,這里說一下常見做法。
想要確認(rèn)用戶信息就不得不提到用戶登錄&認(rèn)證這套東西了,登錄的方式非常多,簡單的有賬號(hào)密碼登錄、手機(jī)驗(yàn)證碼登錄,復(fù)雜的就是單點(diǎn)登錄、三方授權(quán)登錄如微信掃碼、支付寶掃碼等。雖然方式多,但是結(jié)果都一樣的:確認(rèn)當(dāng)前用戶身份
。
當(dāng)前用戶身份確認(rèn)好之后,系統(tǒng)一般會(huì)根據(jù)當(dāng)前用戶信息生成一個(gè)唯一的并帶有時(shí)效性的token,放入下一次請求的cookie中。等到下一次請求來的時(shí)候,我們就可以從cookie中獲取這個(gè)token,利用這個(gè)token獲取這個(gè)用戶的信息。
由于用戶認(rèn)證情況太多,這里我就不貼代碼了,上面是賬號(hào)密碼登錄用戶認(rèn)證的的時(shí)序圖,供大家參考。
以上就是接口用戶上下文的設(shè)計(jì)與實(shí)現(xiàn)的詳細(xì)內(nèi)容,更多關(guān)于接口用戶上下文的設(shè)計(jì)與實(shí)現(xiàn)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot中注解@ConfigurationProperties與@Value的區(qū)別與使用詳解
本文主要介紹了SpringBoot中注解@ConfigurationProperties與@Value的區(qū)別與使用,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09selenium+java破解極驗(yàn)滑動(dòng)驗(yàn)證碼的示例代碼
本篇文章主要介紹了selenium+java破解極驗(yàn)滑動(dòng)驗(yàn)證碼的示例代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-01-01Java之Scanner.nextLine()讀取回車的問題及解決
這篇文章主要介紹了Java之Scanner.nextLine()讀取回車的問題及解決,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04spring項(xiàng)目實(shí)現(xiàn)單元測試過程解析
這篇文章主要介紹了spring項(xiàng)目實(shí)現(xiàn)單元測試過程解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-10-10Java WebService 簡單實(shí)例(附實(shí)例代碼)
本篇文章主要介紹了Java WebService 簡單實(shí)例(附實(shí)例代碼), Web Service 是一種新的web應(yīng)用程序分支,他們是自包含、自描述、模塊化的應(yīng)用,可以發(fā)布、定位、通過web調(diào)用。有興趣的可以了解一下2017-01-01SpringSecurity表單配置之登錄成功及頁面跳轉(zhuǎn)原理解析
這篇文章主要介紹了SpringSecurity表單配置之登錄成功及頁面跳轉(zhuǎn)原理解析,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2023-12-12