小程序靜默登錄方案設計
發表時(shí)間:2021-3-1
發布人(rén):融晨科技
浏覽次數:128
首先談談在(zài)小程序的(de)開發中,如何借助微信的(de)能力标識一個(gè)用戶?
微信官方提供了(le/liǎo)兩種标識:
OpenId
是(shì)一個(gè)用戶對于(yú)一個(gè)小程序/公衆号的(de)标識,開發者可以(yǐ)通過這(zhè)個(gè)标識識别出(chū)用戶。UnionId
是(shì)一個(gè)用戶對于(yú)同主體微信小程序/公衆号/APP 的(de)标識,開發者需要(yào / yāo)在(zài)微信開放平台下綁定相同賬号的(de)主體。開發者可通過UnionId
,實現多個(gè)小程序、公衆号、甚至 APP 之(zhī)間的(de)數據互通。
同一個(gè)用戶的(de)這(zhè)兩個(gè) ID 對于(yú)同一個(gè)小程序來(lái)說(shuō)是(shì)永久不(bù)變的(de),就(jiù)算用戶删了(le/liǎo)小程序,下次用戶進入小程序,開發者依舊可以(yǐ)通過後台的(de)記錄标識出(chū)來(lái)。那麽如何獲取OpenId
和(hé / huò)UnionId
呢?
早期(2018 年 4 月之(zhī)前)的(de)小程序設計使用 wx.getUserInfo
接口,來(lái)獲取用戶信息。設計這(zhè)個(gè)接口的(de)初衷是(shì)希望開發者在(zài)真正需要(yào / yāo)用戶信息(如頭像、昵稱、手機号等)的(de)情況下才去調取這(zhè)個(gè)接口。但很多開發者爲(wéi / wèi)了(le/liǎo)拿到(dào)UnionId
,會在(zài)小程序啓動時(shí)直接調用這(zhè)個(gè)接口,導緻用戶在(zài)使用小程序的(de)時(shí)候産生困擾,歸結起來(lái)有幾點:
- 開發者在(zài)小程序首頁直接調用
wx.getUserInfo
進行授權,彈框獲取用戶信息,會使得一部分用戶點擊“拒絕”按鈕。 - 在(zài)開發者沒有處理用戶拒絕彈框的(de)情況下,用戶必須授權頭像昵稱等信息才能繼續使用小程序,會導緻某些用戶放棄使用該小程序。
- 用戶沒有很好的(de)方式重新授權,盡管微信官方增加了(le/liǎo)設置頁面,可以(yǐ)讓用戶選擇重新授權,但很多用戶并不(bù)知道(dào)可以(yǐ)這(zhè)麽操作。
微信官方也(yě)意識到(dào)了(le/liǎo)這(zhè)個(gè)問題,針對獲取用戶信息更新了(le/liǎo)三個(gè)能力:
- 使用組件來(lái)獲取用戶信息。
- 若用戶滿足一定條件,則可以(yǐ)用
wx.login
獲取到(dào)的(de) code 直接換到(dào)unionId
。 wx.getUserInfo
不(bù)需要(yào / yāo)依賴wx.login
就(jiù)能調用得到(dào)數據。
本文主要(yào / yāo)講述的(de)是(shì)第二點能力,微信官方鼓勵開發者在(zài)不(bù)騷擾用戶的(de)情況下合理獲得unionid
,而(ér)僅在(zài)必要(yào / yāo)時(shí)才向用戶彈窗申請使用昵稱頭像,從而(ér)衍生出(chū)「靜默登錄」和(hé / huò)「用戶登錄」兩種概念。
2. 什麽是(shì)靜默登錄?
小程序可以(yǐ)通過微信官方提供的(de)登錄能力方便地(dì / de)獲取微信提供的(de)用戶身份标識,快速建立小程序内的(de)用戶體系。
很多開發者會把 wx.login
和(hé / huò) wx.getUserInfo
捆綁調用當成登錄使用,其實 wx.login
已經可以(yǐ)完成登錄,wx.getUserInfo
隻是(shì)獲取額外的(de)用戶信息。
在(zài) wx.login
獲取到(dào) code
後,會發送到(dào)開發者後端,開發者後端通過接口去微信後端換取到(dào) openid
和(hé / huò) sessionKey
(現在(zài)會将 unionid
也(yě)一并返回)後,把自定義登錄态 3rd_session
(本業務命名爲(wéi / wèi)auth-token
) 返回給前端,就(jiù)已經完成登錄行爲(wéi / wèi)了(le/liǎo)。wx.login
行爲(wéi / wèi)是(shì)靜默,不(bù)必授權的(de),用戶不(bù)會察覺。
wx.getUserInfo
隻是(shì)爲(wéi / wèi)了(le/liǎo)提供更優質的(de)服務而(ér)存在(zài),比如獲取用戶的(de)手機号注冊會員,或者展示頭像昵稱,判斷性别,開發者可通過 unionId
和(hé / huò)其他(tā)公衆号上(shàng)已有的(de)用戶畫像結合來(lái)提供曆史數據。因此開發者不(bù)必在(zài)用戶剛剛進入小程序的(de)時(shí)候就(jiù)強制要(yào / yāo)求授權。
2.1 靜默登錄流程時(shí)序
官方給出(chū)了(le/liǎo) wx.login
的(de)最佳實踐如下:
靜默登錄英文簡稱爲(wéi / wèi)silentLogin
,代碼如下所示:
private async silentLogin(): Promise<void> {
try {
this.status.silentLogin.ing();
// 獲取臨時(shí)登錄憑證code
const code = await getWxLoginCode();
// 将code發送給服務端
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;
}
}
複制代碼
總結爲(wéi / wèi)以(yǐ)下三步:
- 小程序端調用
wx.login()
獲取 臨時(shí)登錄憑證code
,并回傳到(dào)開發者服務器。 - 服務器端調用
auth.code2Session
接口,換取 用戶唯一标識OpenID
和(hé / huò) 會話密鑰session_key
。 - 開發者服務器可以(yǐ)根據用戶标識來(lái)生成自定義登錄态(例如:
auth-token
),用于(yú)後續業務邏輯中前後端交互時(shí)識别用戶身份。
2.2 開發者後台校驗與解密開放數據
靜默登錄成功後,微信服務器端會下發一個(gè)session_key
給服務端,而(ér)這(zhè)個(gè)會在(zài)需要(yào / yāo)獲取微信開放數據的(de)時(shí)候會用到(dào)。

爲(wéi / wèi)了(le/liǎo)确保開放接口返回用戶數據的(de)安全性,微信會對明文數據進行簽名。開發者可以(yǐ)根據業務需要(yào / yāo)對數據包進行簽名校驗,确保數據的(de)完整性。
- 小程序通過調用接口(如
wx.getUserInfo
)獲取數據時(shí),如果用戶已經授權,接口會同時(shí)返回以(yǐ)下幾個(gè)字段。如用戶未授權,會先彈出(chū)用戶彈窗,用戶點擊同意授權,接口會同時(shí)返回以(yǐ)下幾個(gè)字段。相反如果用戶拒絕授權,将調用失敗。
屬性 | 類型 | 說(shuō)明 |
---|---|---|
userInfo | UserInfo | 用戶信息對象,不(bù)包含 openid 等敏感信息 |
rawData | string | 不(bù)包括敏感信息的(de)原始數據字符串,用于(yú)計算簽名 |
signature | string | 使用 sha1( rawData + sessionkey ) 得到(dào)字符串,用于(yú)校驗用戶信息 |
encryptedData | string | 包括敏感數據在(zài)内的(de)完整用戶信息的(de)加密數據 |
iv | string | 加密算法的(de)初始向量 |
cloudID | string | 敏感數據對應的(de)雲 ID,開通雲開發的(de)小程序才會返回,可通過雲調用直接獲取開放數據 |
- 開發者将
signature
、rawData
發送到(dào)開發者服務器進行校驗。服務器利用用戶對應的(de)session_key
使用相同的(de)算法計算出(chū)簽名signature2
,比對signature
與signature2
即可校驗數據的(de)完整性。開發者服務器告訴前端開發者數據可信,即可安全使用用戶信息數據。 - 如果開發者想要(yào / yāo)獲取敏感數據(如openid,unionID),則将
encryptedData
和(hé / huò)iv
發送到(dào)開發者服務器,由服務器使用session_key
(對稱解密密鑰)進行對稱解密,獲取敏感數據進行存儲并返回給前端開發者。
注意: 因爲(wéi / wèi)需要(yào / yāo)用戶主動觸發才能發起獲取手機号接口,所以(yǐ)該功能不(bù)由 API 來(lái)調用(即上(shàng)述提到(dào)的(de)wx.getUserInfo
是(shì)無法獲取手機号的(de)),需用 button
組件的(de)點擊來(lái)觸發。獲得encryptedData
和(hé / huò)iv
,同樣發送給開發者服務器,由服務器使用session_key
(對稱解密密鑰)進行對稱解密,獲得對應的(de)手機号。
需要(yào / yāo)關注的(de)是(shì),2021年2月23日,微信團隊發布了(le/liǎo)《小程序登錄、用戶信息相關接口調整說(shuō)明》,進行了(le/liǎo)如下調整:
- 2021年2月23日起,通過
wx.login
接口獲取的(de)登錄憑證可直接換取unionID
。 - 2021年4月13日後發布新版本的(de)小程序,無法通過
wx.getUserInfo
接口獲取用戶個(gè)人(rén)信息(頭像、昵稱、性别與地(dì / de)區),将直接獲取匿名數據。getUserInfo
接口獲取加密後的(de)openID
與unionID
數據的(de)能力不(bù)做調整。 - 新增
getUserProfile
接口(基礎庫2.10.4版本開始支持),可獲取用戶頭像、昵稱、性别及地(dì / de)區信息,開發者每次通過該接口獲取用戶個(gè)人(rén)信息均需用戶确認。
即開發者通過組件調用wx.getUserInfo
将不(bù)再彈出(chū)彈窗,直接返回匿名的(de)用戶個(gè)人(rén)信息。如果要(yào / yāo)獲取用戶頭像、昵稱、性别及地(dì / de)區信息,需要(yào / yāo)改造成wx.getUserProfile
接口。
2.3 session_key 的(de)有效期
開發者如果遇到(dào)因爲(wéi / wèi) session_key
不(bù)正确而(ér)校驗簽名失敗或解密失敗,請關注下面幾個(gè)與 session_key
有關的(de)注意事項。
wx.login
調用時(shí),用戶的(de)session_key
可能會被更新而(ér)緻使舊session_key
失效(刷新機制存在(zài)最短周期,如果同一個(gè)用戶短時(shí)間内多次調用wx.login
,并非每次調用都導緻session_key
刷新)。開發者應該在(zài)明确需要(yào / yāo)重新登錄時(shí)才調用wx.login
,及時(shí)通過auth.code2Session
接口更新服務器存儲的(de)session_key
。- 微信不(bù)會把
session_key
的(de)有效期告知開發者。我們會根據用戶使用小程序的(de)行爲(wéi / wèi)對session_key
進行續期。用戶越頻繁使用小程序,session_key
有效期越長。 - 開發者在(zài)
session_key
失效時(shí),可以(yǐ)通過重新執行登錄流程獲取有效的(de)session_key
。使用接口wx.checkSession
可以(yǐ)校驗session_key
是(shì)否有效,從而(ér)避免小程序反複執行登錄流程。 - 當開發者在(zài)實現自定義登錄态時(shí),可以(yǐ)考慮以(yǐ)
session_key
有效期作爲(wéi / wèi)自身登錄态有效期,也(yě)可以(yǐ)實現自定義的(de)時(shí)效性策略。
3. 靜默登錄的(de)調用時(shí)機
3.1 小程序啓動時(shí)調用
由于(yú)大(dà)部分情況都需要(yào / yāo)依賴登錄态,在(zài)小程序啓動的(de)時(shí)候(app.onLaunch()
)調用靜默登錄是(shì)最常見的(de)手段。這(zhè)裏我們封裝一個(gè)login
函數如下所示,首先調用wx.checkSession
判斷session_key
是(shì)否過期,如果session_key
未過期且本地(dì / de)存在(zài)auth_token
自定義登錄态,表示當前的(de)靜默登錄态仍然有效,無需進行其它操作。否則,表示靜默登錄态失效或者新用戶從未發起過靜默登錄,那麽發起靜默登錄流程。
public async login(): Promise<void> {
// 調用wx.checkSession判斷session_key是(shì)否過期
const hasSession = await checkSession();
// 本地(dì / de)已有可用登錄态且session_key未過期,resolve。
if (this.getAuthToken() && hasSession) return Promise.resolve();
// 否則,發起靜默登錄
await this.silentLogin();
}
複制代碼
但是(shì)由于(yú)原生的(de)小程序啓動流程中, App,Page,Component 的(de)生命周期鈎子(zǐ)函數,都不(bù)支持異步阻塞。所以(yǐ)很有可能出(chū)現小程序頁面加載完成後,靜默登錄過程還沒有執行完畢的(de)情況,這(zhè)會導緻後續一些依賴登錄态的(de)操作(比如請求發起)出(chū)錯。
3.2 接口請求發起時(shí)調用
保險起見,如果某些接口需要(yào / yāo)攜帶自定義登錄态進行鑒權,則需要(yào / yāo)在(zài)請求發起時(shí)進行攔截,校驗登錄态。整個(gè)流程如下圖所示:

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

這(zhè)是(shì)因爲(wéi / wèi),我們通過實驗發現,在(zài) session_key
已過期的(de)情況下,wx.checkSession
有一定的(de)幾率返回true
。即增加wx.checkSession
步驟并不(bù)能百分百保證登錄态不(bù)會過期,後續仍然需要(yào / yāo)對不(bù)同的(de)狀态碼進行處理。
所以(yǐ)結論是(shì):wx.checkSession
可靠性是(shì)不(bù)達 100% 的(de)。
基于(yú)以(yǐ)上(shàng),我們需要(yào / yāo)對 session_key
的(de)過期做一些容錯處理:
- 發起需要(yào / yāo)使用
session_key
的(de)請求前,做一次wx.checkSession
操作,如果失敗了(le/liǎo)刷新登錄态。 - 後端使用
session_key
解密開放數據失敗之(zhī)後,返回特定錯誤碼(如:USER_WX_SESSIONKEY_EXPIRE
),前端刷新登錄态。
3.4 并發處理
假設一個(gè)新用戶進入一個(gè)業務複雜的(de)頁面,同時(shí)發起五個(gè)不(bù)同的(de)業務請求,恰巧這(zhè)五個(gè)請求都需要(yào / yāo)鑒權,那麽五個(gè)請求都會被攔截并發起靜默登錄。顯然,這(zhè)樣的(de)并發是(shì)不(bù)合理的(de)。
基于(yú)此,我們設計了(le/liǎo)如下方案:
-
單隊列模式:
- 請求鎖:同一時(shí)間,隻允許一個(gè)正在(zài)過程中的(de)網絡請求。
- 等待隊列:請求被鎖定之(zhī)後,同樣的(de)請求都會被推入隊列,等待進行中的(de)請求返回後,消費同一個(gè)結果。
-
熔斷機制:如果短時(shí)間内多次調用,則停止響應一段時(shí)間,類似于(yú) TCP 慢啓動。


如上(shàng)圖所示,首先refreshLogin
請求入隊,隊列中隻有一個(gè)請求,發送該請求,同時(shí)保險絲計入次數 1,服務端返回請求結果,消費結果。接着又發起一個(gè)refreshLogin
請求,隊列中隻有一個(gè)請求,發送該請求,同時(shí)保險絲計入次數 2。然後又連續發起三個(gè)請求,由于(yú)上(shàng)一個(gè)請求還沒有執行完成,将這(zhè)三個(gè)請求入隊,等待上(shàng)一個(gè)請求結果返回,隊列中的(de)四個(gè)請求消費同一個(gè)結果。由于(yú)觸發熔斷,保險絲重置,停止響應一段時(shí)間。
以(yǐ)上(shàng)兩種方案通過裝飾器模式引入,代碼如下所示,refreshLogin
函數其實是(shì)slientLogin
函數的(de)一層封裝,用于(yú)接口發起時(shí)調用。而(ér)前面提到(dào)的(de)login
函數也(yě)是(shì)slientLogin
函數的(de)一層封裝,用戶小程序啓動時(shí)調用。
@singleQueue({ name: 'refreshLogin' })
@fuseLine({ name: 'refreshLogin' })
public async refreshLogin(): Promise<void> {
try {
// 清除 Session
this.clearSession();
await this.silentLogin();
} catch (error) {
throw error;
}
}
複制代碼
4. 最後
讀到(dào)這(zhè)裏,相信你已經了(le/liǎo)解「靜默登錄」和(hé / huò)「用戶登錄」的(de)區别。「靜默登錄」是(shì)獲取微信登錄态的(de)過程,通過獲取微信提供的(de)用戶身份标識,快速建立小程序内的(de)用戶體系。「用戶登錄」是(shì)用戶授權個(gè)人(rén)開放數據成爲(wéi / wèi)會員的(de)過程,是(shì)指從遊客态轉換成會員态的(de),擁有購買等操作權限。