《優(yōu)化接口設(shè)計的思路》系列:第二篇—接口用戶上下文的設(shè)計與實(shí)現(xiàn)

前言

大家好!我是sum墨,一個一線的底層碼農(nóng),平時喜歡研究和思考一些技術(shù)相關(guān)的問題并整理成文,限于本人水平,如果文章和代碼有表述不當(dāng)之處,還請不吝賜教。

作為一名從業(yè)已達(dá)六年的老碼農(nóng),我的工作主要是開發(fā)后端Java業(yè)務(wù)系統(tǒng),包括各種管理后臺和小程序等。在這些項(xiàng)目中,我設(shè)計過單/多租戶體系系統(tǒng),對接過許多開放平臺,也搞過消息中心這類較為復(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í)用的技巧。

考慮到文字太過寡淡,我先上一張圖

image.png

在Spring Boot中,默認(rèn)情況下,每個請求到達(dá)時都會分配一個單獨(dú)的線程來處理,而且請求的發(fā)起人也不一定都是同一個人,所以一個請求對應(yīng)一個用戶上下文,并且要求線程隔離,即不同線程的用戶上下文互不影響,最后用戶上下文還需要隨著線程的結(jié)束而刪除。
本文我會從用戶上下文如何構(gòu)建、如何使用、如何刪除這三個方面解釋接口用戶上下文的設(shè)計與實(shí)現(xiàn)。

本文參考項(xiàng)目源碼地址:summo-springboot-interface-demo

由于文章經(jīng)常被抄襲,開源的代碼甚至被當(dāng)成收費(fèi)項(xiàng),所以源碼里面不是全部代碼,有需要的同學(xué)可以留個郵箱,我給你單獨(dú)發(fā)!

一、接口用戶上下文的構(gòu)建、使用、清除

1. 利用Filter攔截到每一個請求

由于接口散落在各個Controller中,且絕大部分接口都是需要這個用戶上下文的(注:也不排除不需要用戶上下文的接口存在),所以這里需要統(tǒng)一入口進(jìn)行創(chuàng)建、銷毀??雌饋砜梢允褂肁OP的方式來實(shí)現(xiàn),
不過這里有一個更合適的方案,利用SpringBoot自帶的Filter【javax.servlet.Filter】來實(shí)現(xiàn)。

實(shí)現(xiàn)起來非常簡單,我這邊自定義了一個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,這行一定要加,否則程序會中斷掉
            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)
我們可以在這個方法里面獲取到ServletRequest和ServletResponse,這兩個類能獲取到代表著我們可以操作整個請求過程,這里如何確定當(dāng)前請求的用戶?下面有一張流程圖供大家參考:

image.png

還有一種做法是使用JWT來當(dāng)做用戶token,因?yàn)镴WT本身就可以存儲一些信息,所以我們就不需要去緩存用戶信息了,直接解析JWT即可,這種做法在分布式應(yīng)用中很常見。

2. 獲取當(dāng)前請求的線程

上面已經(jīng)獲取到用戶信息了,現(xiàn)在需要將用戶信息放入用戶上下文中,但由于請求的發(fā)起人不一定都是同一個人,所以一個請求對應(yīng)著一個用戶上下文,也即一個線程設(shè)置一個上下文。那么這里就需要獲取到當(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一個對象就行,而且支持泛型。

3. 用戶上下文生命周期管理

對于用戶上下文的生命周期管理需要定義3個方法:

  • 設(shè)置上下文用戶信息;
  • 獲取上下文用戶信息
  • 清除上下文用戶信息

以上方法均為靜態(tài)方法。

下面是一個簡單的例子:
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è)計的思路》系列:第三篇—在用戶使用系統(tǒng)過程中留下痕跡 的方法三.

c. 防止接口數(shù)據(jù)越權(quán)

舉個例子,比如有些業(yè)務(wù)需要獲取當(dāng)前登錄用戶的信息、當(dāng)前登錄用戶的收藏、當(dāng)前登錄用戶的瀏覽記錄,這樣的接口總不能在接口上傳一個userId吧?真要這樣干了,非得給安全罵死。。。
利用用戶上下文的話,接口就可以不用傳遞任何參數(shù)獲取到當(dāng)前用戶的userId,實(shí)現(xiàn)你的需求啦。

d. 跨服務(wù)調(diào)用

在分布式系統(tǒng)中,可以將用戶上下文信息傳遞給其他服務(wù),以保持用戶的一致性和連貫性。

e. 監(jiān)控和統(tǒng)計

可以將用戶上下文中的信息用于系統(tǒng)的監(jiān)控和統(tǒng)計,如請求的處理時間、請求的次數(shù)等。

5. 用戶上下文的刪除

刪除很簡單,調(diào)用GlobalUserContext.clear();即可,詳情可見WebFilter.java內(nèi)容。

二. 用戶登錄&認(rèn)證

上面主要是說怎么獲取到接口請求的用戶以及怎么設(shè)置用戶上下文,但沒說用戶身份是什么時候確認(rèn)的以及怎么確認(rèn)的,這里說一下常見做法。
想要確認(rèn)用戶信息就不得不提到用戶登錄&認(rèn)證這套東西了,登錄的方式非常多,簡單的有賬號密碼登錄、手機(jī)驗(yàn)證碼登錄,復(fù)雜的就是單點(diǎn)登錄、三方授權(quán)登錄如微信掃碼、支付寶掃碼等。雖然方式多,但是結(jié)果都一樣的:確認(rèn)當(dāng)前用戶身份。

當(dāng)前用戶身份確認(rèn)好之后,系統(tǒng)一般會根據(jù)當(dāng)前用戶信息生成一個唯一的并帶有時效性的token,放入下一次請求的cookie中。等到下一次請求來的時候,我們就可以從cookie中獲取這個token,利用這個token獲取這個用戶的信息。

由于用戶認(rèn)證情況太多,這里我就不貼代碼了,上面是賬號密碼登錄用戶認(rèn)證的的時序圖,供大家參考。

image.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容