小程序靜默登錄方案設(shè)計(jì)
點(diǎn)擊上方 程序員成長(zhǎng)指北,關(guān)注公眾號(hào)
回復(fù)1,加入Node進(jìn)階交流群
來(lái)源:蔡小真
https://juejin.cn/post/6933082931653148680
1. 背景
首先談?wù)勗谛〕绦虻拈_(kāi)發(fā)中,如何借助微信的能力標(biāo)識(shí)一個(gè)用戶(hù)?
微信官方提供了兩種標(biāo)識(shí):
OpenId是一個(gè)用戶(hù)對(duì)于一個(gè)小程序/公眾號(hào)的標(biāo)識(shí),開(kāi)發(fā)者可以通過(guò)這個(gè)標(biāo)識(shí)識(shí)別出用戶(hù)。UnionId是一個(gè)用戶(hù)對(duì)于同主體微信小程序/公眾號(hào)/APP 的標(biāo)識(shí),開(kāi)發(fā)者需要在微信開(kāi)放平臺(tái)下綁定相同賬號(hào)的主體。開(kāi)發(fā)者可通過(guò)UnionId,實(shí)現(xiàn)多個(gè)小程序、公眾號(hào)、甚至 APP 之間的數(shù)據(jù)互通。
同一個(gè)用戶(hù)的這兩個(gè) ID 對(duì)于同一個(gè)小程序來(lái)說(shuō)是永久不變的,就算用戶(hù)刪了小程序,下次用戶(hù)進(jìn)入小程序,開(kāi)發(fā)者依舊可以通過(guò)后臺(tái)的記錄標(biāo)識(shí)出來(lái)。那么如何獲取OpenId和UnionId呢?
早期(2018 年 4 月之前)的小程序設(shè)計(jì)使用 wx.getUserInfo 接口,來(lái)獲取用戶(hù)信息。設(shè)計(jì)這個(gè)接口的初衷是希望開(kāi)發(fā)者在真正需要用戶(hù)信息(如頭像、昵稱(chēng)、手機(jī)號(hào)等)的情況下才去調(diào)取這個(gè)接口。但很多開(kāi)發(fā)者為了拿到UnionId,會(huì)在小程序啟動(dòng)時(shí)直接調(diào)用這個(gè)接口,導(dǎo)致用戶(hù)在使用小程序的時(shí)候產(chǎn)生困擾,歸結(jié)起來(lái)有幾點(diǎn):
開(kāi)發(fā)者在小程序首頁(yè)直接調(diào)用 wx.getUserInfo進(jìn)行授權(quán),彈框獲取用戶(hù)信息,會(huì)使得一部分用戶(hù)點(diǎn)擊“拒絕”按鈕。在開(kāi)發(fā)者沒(méi)有處理用戶(hù)拒絕彈框的情況下,用戶(hù)必須授權(quán)頭像昵稱(chēng)等信息才能繼續(xù)使用小程序,會(huì)導(dǎo)致某些用戶(hù)放棄使用該小程序。 用戶(hù)沒(méi)有很好的方式重新授權(quán),盡管微信官方增加了設(shè)置頁(yè)面,可以讓用戶(hù)選擇重新授權(quán),但很多用戶(hù)并不知道可以這么操作。
微信官方也意識(shí)到了這個(gè)問(wèn)題,針對(duì)獲取用戶(hù)信息更新了三個(gè)能力:
使用組件來(lái)獲取用戶(hù)信息。 若用戶(hù)滿足一定條件,則可以用 wx.login獲取到的 code 直接換到unionId。wx.getUserInfo不需要依賴(lài)wx.login就能調(diào)用得到數(shù)據(jù)。
本文主要講述的是第二點(diǎn)能力,微信官方鼓勵(lì)開(kāi)發(fā)者在不騷擾用戶(hù)的情況下合理獲得unionid,而僅在必要時(shí)才向用戶(hù)彈窗申請(qǐng)使用昵稱(chēng)頭像,從而衍生出「靜默登錄」和「用戶(hù)登錄」兩種概念。
2. 什么是靜默登錄?
小程序可以通過(guò)微信官方提供的登錄能力方便地獲取微信提供的用戶(hù)身份標(biāo)識(shí),快速建立小程序內(nèi)的用戶(hù)體系。
很多開(kāi)發(fā)者會(huì)把 wx.login 和 wx.getUserInfo 捆綁調(diào)用當(dāng)成登錄使用,其實(shí) wx.login 已經(jīng)可以完成登錄,wx.getUserInfo 只是獲取額外的用戶(hù)信息。
在 wx.login 獲取到 code 后,會(huì)發(fā)送到開(kāi)發(fā)者后端,開(kāi)發(fā)者后端通過(guò)接口去微信后端換取到 openid 和 sessionKey(現(xiàn)在會(huì)將 unionid 也一并返回)后,把自定義登錄態(tài) 3rd_session(本業(yè)務(wù)命名為auth-token) 返回給前端,就已經(jīng)完成登錄行為了。wx.login 行為是靜默,不必授權(quán)的,用戶(hù)不會(huì)察覺(jué)。
wx.getUserInfo 只是為了提供更優(yōu)質(zhì)的服務(wù)而存在,比如獲取用戶(hù)的手機(jī)號(hào)注冊(cè)會(huì)員,或者展示頭像昵稱(chēng),判斷性別,開(kāi)發(fā)者可通過(guò) unionId 和其他公眾號(hào)上已有的用戶(hù)畫(huà)像結(jié)合來(lái)提供歷史數(shù)據(jù)。因此開(kāi)發(fā)者不必在用戶(hù)剛剛進(jìn)入小程序的時(shí)候就強(qiáng)制要求授權(quán)。
2.1 靜默登錄流程時(shí)序
官方給出了 wx.login 的最佳實(shí)踐如下:

靜默登錄英文簡(jiǎn)稱(chēng)為silentLogin,代碼如下所示:
private async silentLogin(): Promise<void> {
try {
this.status.silentLogin.ing();
// 獲取臨時(shí)登錄憑證code
const code = await getWxLoginCode();
// 將code發(fā)送給服務(wù)端
const res = await API.login(code);
// 保存登錄信息,如auth-token
storage.setSync(constant.STORAGE_SESSION_KEY, res.data);
this.status.silentLogin.success();
} catch (error) {
logger.error('靜默登錄失敗', error);
this.status.silentLogin.fail(error);
throw error;
}
}
復(fù)制代碼
總結(jié)為以下三步:
小程序端調(diào)用 wx.login()獲取 臨時(shí)登錄憑證code,并回傳到開(kāi)發(fā)者服務(wù)器。服務(wù)器端調(diào)用 auth.code2Session接口,換取 用戶(hù)唯一標(biāo)識(shí)OpenID和 會(huì)話密鑰session_key。開(kāi)發(fā)者服務(wù)器可以根據(jù)用戶(hù)標(biāo)識(shí)來(lái)生成自定義登錄態(tài)(例如: auth-token),用于后續(xù)業(yè)務(wù)邏輯中前后端交互時(shí)識(shí)別用戶(hù)身份。
2.2 開(kāi)發(fā)者后臺(tái)校驗(yàn)與解密開(kāi)放數(shù)據(jù)
靜默登錄成功后,微信服務(wù)器端會(huì)下發(fā)一個(gè)session_key給服務(wù)端,而這個(gè)會(huì)在需要獲取微信開(kāi)放數(shù)據(jù)的時(shí)候會(huì)用到。

為了確保開(kāi)放接口返回用戶(hù)數(shù)據(jù)的安全性,微信會(huì)對(duì)明文數(shù)據(jù)進(jìn)行簽名。開(kāi)發(fā)者可以根據(jù)業(yè)務(wù)需要對(duì)數(shù)據(jù)包進(jìn)行簽名校驗(yàn),確保數(shù)據(jù)的完整性。
小程序通過(guò)調(diào)用接口(如 wx.getUserInfo)獲取數(shù)據(jù)時(shí),如果用戶(hù)已經(jīng)授權(quán),接口會(huì)同時(shí)返回以下幾個(gè)字段。如用戶(hù)未授權(quán),會(huì)先彈出用戶(hù)彈窗,用戶(hù)點(diǎn)擊同意授權(quán),接口會(huì)同時(shí)返回以下幾個(gè)字段。相反如果用戶(hù)拒絕授權(quán),將調(diào)用失敗。
| 屬性 | 類(lèi)型 | 說(shuō)明 |
|---|---|---|
userInfo | UserInfo | 用戶(hù)信息對(duì)象,不包含 openid 等敏感信息 |
rawData | string | 不包括敏感信息的原始數(shù)據(jù)字符串,用于計(jì)算簽名 |
signature | string | 使用 sha1( rawData + sessionkey ) 得到字符串,用于校驗(yàn)用戶(hù)信息 |
encryptedData | string | 包括敏感數(shù)據(jù)在內(nèi)的完整用戶(hù)信息的加密數(shù)據(jù) |
iv | string | 加密算法的初始向量 |
cloudID | string | 敏感數(shù)據(jù)對(duì)應(yīng)的云 ID,開(kāi)通云開(kāi)發(fā)的小程序才會(huì)返回,可通過(guò)云調(diào)用直接獲取開(kāi)放數(shù)據(jù) |
開(kāi)發(fā)者將 signature、rawData發(fā)送到開(kāi)發(fā)者服務(wù)器進(jìn)行校驗(yàn)。服務(wù)器利用用戶(hù)對(duì)應(yīng)的session_key使用相同的算法計(jì)算出簽名signature2,比對(duì)signature與signature2即可校驗(yàn)數(shù)據(jù)的完整性。開(kāi)發(fā)者服務(wù)器告訴前端開(kāi)發(fā)者數(shù)據(jù)可信,即可安全使用用戶(hù)信息數(shù)據(jù)。如果開(kāi)發(fā)者想要獲取敏感數(shù)據(jù)(如 openid,unionID),則將 encryptedData和iv發(fā)送到開(kāi)發(fā)者服務(wù)器,由服務(wù)器使用session_key(對(duì)稱(chēng)解密密鑰)進(jìn)行對(duì)稱(chēng)解密,獲取敏感數(shù)據(jù)進(jìn)行存儲(chǔ)并返回給前端開(kāi)發(fā)者。
注意: 因?yàn)樾枰脩?hù)主動(dòng)觸發(fā)才能發(fā)起獲取手機(jī)號(hào)接口,所以該功能不由 API 來(lái)調(diào)用(即上述提到的wx.getUserInfo是無(wú)法獲取手機(jī)號(hào)的),需用 button 組件的點(diǎn)擊來(lái)觸發(fā)。獲得encryptedData和iv,同樣發(fā)送給開(kāi)發(fā)者服務(wù)器,由服務(wù)器使用session_key(對(duì)稱(chēng)解密密鑰)進(jìn)行對(duì)稱(chēng)解密,獲得對(duì)應(yīng)的手機(jī)號(hào)。
需要關(guān)注的是,2021 年 2 月 23 日,微信團(tuán)隊(duì)發(fā)布了《小程序登錄、用戶(hù)信息相關(guān)接口調(diào)整說(shuō)明》,進(jìn)行了如下調(diào)整:
2021 年 2 月 23 日起,通過(guò) wx.login接口獲取的登錄憑證可直接換取unionID。2021 年 4 月 13 日后發(fā)布新版本的小程序,無(wú)法通過(guò) wx.getUserInfo接口獲取用戶(hù)個(gè)人信息(頭像、昵稱(chēng)、性別與地區(qū)),將直接獲取匿名數(shù)據(jù)。getUserInfo接口獲取加密后的openID與unionID數(shù)據(jù)的能力不做調(diào)整。新增 getUserProfile接口(基礎(chǔ)庫(kù) 2.10.4 版本開(kāi)始支持),可獲取用戶(hù)頭像、昵稱(chēng)、性別及地區(qū)信息,開(kāi)發(fā)者每次通過(guò)該接口獲取用戶(hù)個(gè)人信息均需用戶(hù)確認(rèn)。
即開(kāi)發(fā)者通過(guò)組件調(diào)用wx.getUserInfo將不再?gòu)棾鰪棿?,直接返回匿名的用?hù)個(gè)人信息。如果要獲取用戶(hù)頭像、昵稱(chēng)、性別及地區(qū)信息,需要改造成wx.getUserProfile接口。
2.3 session_key 的有效期
開(kāi)發(fā)者如果遇到因?yàn)?nbsp;session_key 不正確而校驗(yàn)簽名失敗或解密失敗,請(qǐng)關(guān)注下面幾個(gè)與 session_key 有關(guān)的注意事項(xiàng)。
wx.login調(diào)用時(shí),用戶(hù)的session_key可能會(huì)被更新而致使舊session_key失效(刷新機(jī)制存在最短周期,如果同一個(gè)用戶(hù)短時(shí)間內(nèi)多次調(diào)用wx.login,并非每次調(diào)用都導(dǎo)致session_key刷新)。開(kāi)發(fā)者應(yīng)該在明確需要重新登錄時(shí)才調(diào)用wx.login,及時(shí)通過(guò)auth.code2Session接口更新服務(wù)器存儲(chǔ)的session_key。微信不會(huì)把 session_key的有效期告知開(kāi)發(fā)者。我們會(huì)根據(jù)用戶(hù)使用小程序的行為對(duì)session_key進(jìn)行續(xù)期。用戶(hù)越頻繁使用小程序,session_key有效期越長(zhǎng)。開(kāi)發(fā)者在 session_key失效時(shí),可以通過(guò)重新執(zhí)行登錄流程獲取有效的session_key。使用接口wx.checkSession可以校驗(yàn)session_key是否有效,從而避免小程序反復(fù)執(zhí)行登錄流程。當(dāng)開(kāi)發(fā)者在實(shí)現(xiàn)自定義登錄態(tài)時(shí),可以考慮以 session_key有效期作為自身登錄態(tài)有效期,也可以實(shí)現(xiàn)自定義的時(shí)效性策略。
3 「登錄」架構(gòu)

「登錄」方案架構(gòu)如上圖所示,將所有登錄相關(guān)功能抽象到 「service 層」(本項(xiàng)目將其命名為session),供 「業(yè)務(wù)層」 調(diào)用。本文主要講述灰色內(nèi)容,其它模塊將在下一篇文章《小程序用戶(hù)登錄設(shè)計(jì)》中闡述。
3.1 libs - 提供登錄相關(guān)的類(lèi)方法供「業(yè)務(wù)層」調(diào)用
封裝 session類(lèi),提供類(lèi)方法供「業(yè)務(wù)層」調(diào)用。主要有以下幾種方法:
| 方法名 | 功能 | 使用場(chǎng)景 |
|---|---|---|
silentLogin | 發(fā)起靜默登錄 | - |
login | 登錄,silentLogin 方法的一層封裝 | 用于小程序啟動(dòng)時(shí)發(fā)起靜默登錄 |
refreshLogin | 刷新登錄態(tài),silentLogin 方法的一層封裝 | 用于登錄態(tài)過(guò)期時(shí)發(fā)起靜默登錄 |
ensureSessionKey | 驗(yàn)證 sessionKey 是否過(guò)期,過(guò)期則刷新登錄態(tài) | 綁定微信授權(quán)手機(jī)號(hào)時(shí)驗(yàn)證是否過(guò)期,過(guò)期則得重新彈窗授權(quán) |
裝飾器:
fuse-line:熔斷機(jī)制,如果短時(shí)間內(nèi)多次調(diào)用,則停止響應(yīng)一段時(shí)間,類(lèi)似于 TCP 慢啟動(dòng)。用于解決refreshLogin、login等方法的并發(fā)處理問(wèn)題。single-queue:?jiǎn)侮?duì)列模式,同一時(shí)間,只允許一個(gè)正在過(guò)程中的網(wǎng)絡(luò)請(qǐng)求。請(qǐng)求被鎖定之后,同樣的請(qǐng)求都會(huì)被推入隊(duì)列,等待進(jìn)行中的請(qǐng)求返回后,消費(fèi)同一個(gè)結(jié)果。用于解決refreshLogin、login等方法的并發(fā)處理問(wèn)題。
4. 靜默登錄的調(diào)用時(shí)機(jī)
4.1 小程序啟動(dòng)時(shí)調(diào)用
由于大部分情況都需要依賴(lài)登錄態(tài),在小程序啟動(dòng)的時(shí)候(app.onLaunch())調(diào)用靜默登錄是最常見(jiàn)的手段。這里我們封裝一個(gè)login函數(shù)如下所示,首先調(diào)用wx.checkSession判斷session_key是否過(guò)期,如果session_key未過(guò)期且本地存在auth_token自定義登錄態(tài),表示當(dāng)前的靜默登錄態(tài)仍然有效,無(wú)需進(jìn)行其它操作。否則,表示靜默登錄態(tài)失效或者新用戶(hù)從未發(fā)起過(guò)靜默登錄,那么發(fā)起靜默登錄流程。
public async login(): Promise<void> {
// 調(diào)用wx.checkSession判斷session_key是否過(guò)期
const hasSession = await checkSession();
// 本地已有可用登錄態(tài)且session_key未過(guò)期,resolve。
if (this.getAuthToken() && hasSession) return Promise.resolve();
// 否則,發(fā)起靜默登錄
await this.silentLogin();
}
復(fù)制代碼
但是由于原生的小程序啟動(dòng)流程中, App,Page,Component 的生命周期鉤子函數(shù),都不支持異步阻塞。所以很有可能出現(xiàn)小程序頁(yè)面加載完成后,靜默登錄過(guò)程還沒(méi)有執(zhí)行完畢的情況,這會(huì)導(dǎo)致后續(xù)一些依賴(lài)登錄態(tài)的操作(比如請(qǐng)求發(fā)起)出錯(cuò)。
4.2 接口請(qǐng)求發(fā)起時(shí)調(diào)用
保險(xiǎn)起見(jiàn),如果某些接口需要攜帶自定義登錄態(tài)進(jìn)行鑒權(quán),則需要在請(qǐng)求發(fā)起時(shí)進(jìn)行攔截,校驗(yàn)登錄態(tài),并刷新登錄。刷新登錄代碼如下所示:
public async refreshLogin(): Promise<void> {
try {
// 清除 Session
this.clearSession();
// 發(fā)起靜默登錄
await this.silentLogin();
} catch (error) {
throw error;
}
}
復(fù)制代碼
整個(gè)流程如下圖所示:

攔截 request: 判斷是否需要鑒權(quán):請(qǐng)求發(fā)起時(shí),攔截請(qǐng)求,判斷請(qǐng)求是否需要添加 auth-token,如若不需要,直接發(fā)起請(qǐng)求。如若需要,執(zhí)行第二步。判斷是否需要發(fā)起靜默登錄:判斷 storage中是否存在auth-token,如若不存在,發(fā)起「刷新登錄」。請(qǐng)求頭部添加 auth-token:添加auth-token,發(fā)起請(qǐng)求。與服務(wù)端通信:發(fā)起請(qǐng)求,服務(wù)端處理請(qǐng)求返回結(jié)果。 攔截 response: 解析狀態(tài)碼 狀態(tài)碼為 AUTH_FAIL:服務(wù)端返回code為“鑒權(quán)失敗”,觸發(fā)這種情景的原因有兩個(gè),一是接口需要鑒權(quán),但是發(fā)起請(qǐng)求時(shí)未攜帶auth-token,二是auth-token過(guò)期。這時(shí)將上一次請(qǐng)求攜帶的auth-token與本地存儲(chǔ)的auth-token比較,如果不一致,表示登錄態(tài)已經(jīng)刷新過(guò)了,那么就直接重新發(fā)起請(qǐng)求。如果一致,發(fā)起刷新登錄,拿到新的auth-token后重新發(fā)起請(qǐng)求,這個(gè)動(dòng)作對(duì)用戶(hù)來(lái)說(shuō)是無(wú)感知的。狀態(tài)碼為 USER_WX_SESSIONKEY_EXPIRE:服務(wù)器返回code為“用戶(hù)登錄態(tài)過(guò)期”,這是針對(duì)用戶(hù)授權(quán)手機(jī)號(hào)登錄失敗定制的狀態(tài)碼,如果登錄態(tài)已過(guò)期,表示存儲(chǔ)在服務(wù)端的session_key也是過(guò)期的,那么點(diǎn)擊授權(quán)手機(jī)號(hào)獲取的加密數(shù)據(jù)發(fā)送到服務(wù)端進(jìn)行對(duì)稱(chēng)解密,由于session_key失效,無(wú)法解密出真正的手機(jī)號(hào)。因此需要重新發(fā)起靜默登錄,等待用戶(hù)重新點(diǎn)擊授權(quán)按鈕獲取新的加密數(shù)據(jù),然后發(fā)起新的解密請(qǐng)求狀態(tài)碼為其它:比如 Success或者其他業(yè)務(wù)請(qǐng)求錯(cuò)誤的情況,不進(jìn)行攔截,返回 response 讓業(yè)務(wù)代碼解析。
4.3 wx.checkSession 罷工之謎
基于上述接口請(qǐng)求發(fā)起時(shí)調(diào)用的流程,很多人會(huì)有疑問(wèn),既然服務(wù)端會(huì)返回auth-token過(guò)期的狀態(tài)碼,為啥不在請(qǐng)求發(fā)送前進(jìn)行攔截,使用wx.checkSession接口校驗(yàn)登錄態(tài)是否過(guò)期(如下圖所示,增加紅框內(nèi)的步驟)?

這是因?yàn)?,我們通過(guò)實(shí)驗(yàn)發(fā)現(xiàn),在 session_key 已過(guò)期的情況下,wx.checkSession 有一定的幾率返回true。即增加wx.checkSession步驟并不能百分百保證登錄態(tài)不會(huì)過(guò)期,后續(xù)仍然需要對(duì)不同的狀態(tài)碼進(jìn)行處理。
社區(qū)也有相關(guān)的反饋未得到解決:
小程序解密手機(jī)號(hào),隔一小段時(shí)間后,checksession:ok,但是解密失敗 wx.checkSession 有效,但是解密數(shù)據(jù)失敗 checkSession 判斷 session_key 未失效,但是解密手機(jī)號(hào)失敗
所以結(jié)論是:wx.checkSession可靠性是不達(dá) 100% 的。
基于以上,我們需要對(duì) session_key 的過(guò)期做一些容錯(cuò)處理:
發(fā)起需要使用 session_key的請(qǐng)求前,做一次wx.checkSession操作,如果失敗了刷新登錄態(tài)。后端使用 session_key解密開(kāi)放數(shù)據(jù)失敗之后,返回特定錯(cuò)誤碼(如:USER_WX_SESSIONKEY_EXPIRE),前端刷新登錄態(tài)。
4.4 并發(fā)處理
我們知道,當(dāng)啟動(dòng)小程序時(shí),各種監(jiān)控、埋點(diǎn)數(shù)據(jù)上報(bào)都需要獲取用戶(hù)的個(gè)人信息,這些信息都得「靜默登錄」后才能獲取,因此會(huì)同時(shí)發(fā)起多個(gè)login請(qǐng)求。另一種情況下,假設(shè)一個(gè)新用戶(hù)進(jìn)入一個(gè)業(yè)務(wù)復(fù)雜的頁(yè)面,同時(shí)發(fā)起五個(gè)不同的業(yè)務(wù)請(qǐng)求,恰巧這五個(gè)請(qǐng)求都需要鑒權(quán),那么五個(gè)請(qǐng)求都會(huì)被攔截并發(fā)起refreshLogin請(qǐng)求。顯然,這樣的并發(fā)是不合理的。
基于此,我們?cè)O(shè)計(jì)了如下方案:
單隊(duì)列模式:
請(qǐng)求鎖:同一時(shí)間,只允許一個(gè)正在過(guò)程中的網(wǎng)絡(luò)請(qǐng)求。
等待隊(duì)列:請(qǐng)求被鎖定之后,同樣的請(qǐng)求都會(huì)被推入隊(duì)列,等待進(jìn)行中的請(qǐng)求返回后,消費(fèi)同一個(gè)結(jié)果。
熔斷機(jī)制:如果短時(shí)間內(nèi)多次調(diào)用,則停止響應(yīng)一段時(shí)間,類(lèi)似于 TCP 慢啟動(dòng)。


如上圖所示,首先refreshLogin請(qǐng)求入隊(duì),隊(duì)列中只有一個(gè)請(qǐng)求,發(fā)送該請(qǐng)求,同時(shí)保險(xiǎn)絲計(jì)入次數(shù) 1,服務(wù)端返回請(qǐng)求結(jié)果,消費(fèi)結(jié)果。接著又發(fā)起一個(gè)refreshLogin請(qǐng)求,隊(duì)列中只有一個(gè)請(qǐng)求,發(fā)送該請(qǐng)求,同時(shí)保險(xiǎn)絲計(jì)入次數(shù) 2。然后又連續(xù)發(fā)起三個(gè)請(qǐng)求,由于上一個(gè)請(qǐng)求還沒(méi)有執(zhí)行完成,將這三個(gè)請(qǐng)求入隊(duì),等待上一個(gè)請(qǐng)求結(jié)果返回,隊(duì)列中的四個(gè)請(qǐng)求消費(fèi)同一個(gè)結(jié)果。由于觸發(fā)自動(dòng)冷卻閾值,保險(xiǎn)絲重置。
以上兩種方案通過(guò)裝飾器模式引入,代碼如下所示,refreshLogin函數(shù)其實(shí)是slientLogin函數(shù)的一層封裝,用于接口發(fā)起時(shí)調(diào)用。而前面提到的login函數(shù)也是slientLogin函數(shù)的一層封裝,用戶(hù)小程序啟動(dòng)時(shí)調(diào)用。
@singleQueue({ name: 'refreshLogin' })
@fuseLine({ name: 'refreshLogin' })
public async refreshLogin(): Promise<void> {
try {
// 清除 Session
this.clearSession();
await this.silentLogin();
} catch (error) {
throw error;
}
}
復(fù)制代碼
到此,很多讀者可能對(duì)熔斷機(jī)制還不甚理解,熔斷的目的是為一個(gè)函數(shù)提供保險(xiǎn)絲保障,短時(shí)間內(nèi)多次調(diào)用,會(huì)熔斷一段時(shí)間,這段時(shí)間內(nèi)拒絕所有請(qǐng)求。如果在自動(dòng)冷卻閾值內(nèi),沒(méi)有請(qǐng)求通過(guò),則重置保險(xiǎn)絲。代碼如下所示:
export default function fuseLine({
// 一次熔斷前重試次數(shù)
tryTimes = 3,
// 重試間隔,單位 ms
restoreTime = 5000,
// 自動(dòng)冷卻閾值,單位 ms
coolDownThreshold = 1000,
// 名稱(chēng)
name = 'unnamed',
}: {
tryTimes?: number;
restoreTime?: number;
name?: string;
coolDownThreshold?: number;
} = {}) {
// 請(qǐng)求鎖
let fuseLocked = false;
// 當(dāng)前重試次數(shù)
let fuseTryTimes = tryTimes;
// 自動(dòng)冷卻
let coolDownTimer;
// 重置保險(xiǎn)絲
const reset = () => {
fuseLocked = false;
fuseTryTimes = tryTimes;
logger.info(`${name}-保險(xiǎn)絲重置`);
};
const request = async () => {
if (fuseLocked) throw new Error(`${name}-保險(xiǎn)絲已熔斷,請(qǐng)稍后重試`);
// 已達(dá)最大重試次數(shù)
if (fuseTryTimes <= 0) {
fuseLocked = true;
// 重置保險(xiǎn)絲
setTimeout(() => reset(), restoreTime);
throw new Error(`${name}-保險(xiǎn)絲熔斷!!`);
}
// 自動(dòng)冷卻系統(tǒng)
if (coolDownTimer) clearTimeout(coolDownTimer);
coolDownTimer = setTimeout(() => reset(), coolDownThreshold);
// 允許當(dāng)前請(qǐng)求通過(guò)保險(xiǎn)絲,記錄 +1
fuseTryTimes = fuseTryTimes - 1;
logger.info(`${name}-通過(guò)保險(xiǎn)絲(${tryTimes - fuseTryTimes}/${tryTimes})`);
return Promise.resolve();
};
return function(
_target: Record<string, any>,
_propertyName: string,
descriptor: TypedPropertyDescriptor<(...args: any[]) => any>,
) {
const method = descriptor.value;
descriptor.value = async function(...args: any[]) {
await request();
if (method) return method.apply(this, args);
};
};
}
復(fù)制代碼
5. 最后
讀到這里,相信你已經(jīng)了解「靜默登錄」和「用戶(hù)登錄」的區(qū)別?!胳o默登錄」是獲取微信登錄態(tài)的過(guò)程,通過(guò)獲取微信提供的用戶(hù)身份標(biāo)識(shí),快速建立小程序內(nèi)的用戶(hù)體系?!赣脩?hù)登錄」是用戶(hù)授權(quán)個(gè)人開(kāi)放數(shù)據(jù)成為會(huì)員的過(guò)程,是指從游客態(tài)轉(zhuǎn)換成會(huì)員態(tài)的,擁有購(gòu)買(mǎi)等操作權(quán)限。
兩者并不是一個(gè)概念,「用戶(hù)登錄」會(huì)在下一篇文章《小程序用戶(hù)登錄架構(gòu)設(shè)計(jì)》中進(jìn)行闡述。


“分享、點(diǎn)贊、在看” 支持一波
