每一個有數據交互的小程序,都會涉及到登錄、token 等問題,openid 又是什么呢?怎么使用靜默續期,來提升用戶體驗呢?
小程序登錄
登錄時序
一切的一切,都要從這么一張小程序登錄時序圖說起:
通常情況下,我們的小程序都會有業務身份,如何將微信帳號和業務身份關聯起來呢?這個時候我們需要上圖的步驟:
- 小程序調用
wx.login()
獲取臨時登錄憑證code
。 - 小程序將
code
傳到開發者服務器。 - 開發者服務器以
code
換取用戶唯一標識openid
和會話密鑰session_key
。 - 開發者服務器可綁定微信用戶身份
id
和業務用戶身份。 - 開發者服務器可以根據用戶標識來生成自定義登錄態,用於后續業務邏輯中前后端交互時識別用戶身份。
相關數據或參數
上面的登錄時序中,我們會涉及到一些數據和參數,先來了解下它們都是用來做啥的。
臨時登錄憑證 code
在小程序中調用wx.login()
,能拿到一個code
作為用戶登錄憑證(有效期五分鍾)。在開發者服務器后台,開發者可使用code
換取openid
和session_key
等信息(code
只能使用一次)。
code
的設計,主要用於防止黑客使用窮舉等方式把業務側個人信息數據全拉走。
AppId 與 AppSecret
為了確保拿code
過來換取身份信息的人就是對應的小程序開發者,到微信服務器的請求要同時帶上AppId
和AppSecret
。
session_key
會話密鑰session_key
是對用戶數據進行加密簽名的密鑰。為了應用自身的數據安全,開發者服務器不應該把會話密鑰下發到小程序,也不應該對外提供這個密鑰。
設計session_key
主要是為了節省流程消耗,如果每次都通過小程序前端wx.login()
生成微信登錄憑證code
去微信服務器請求信息,步驟太多會造成整體耗時比較嚴重。
使用接口wx.checkSession()
可以校驗session_key
是否有效。用戶越頻繁使用小程序,session_key
有效期越長。session_key
失效時,可以通過重新執行登錄流程獲取有效的session_key
。
openidopenid
是微信用戶id
,可以用這個id
來區分不同的微信用戶。
微信針對不同的用戶在不同的應用下都有唯一的一個openid
, 但是要想確定用戶是不是同一個用戶,就需要靠unionid
來區分。
unionid
如果開發者擁有多個移動應用、網站應用、和公眾帳號(包括小程序),可通過unionid
來區分用戶的唯一性。同一用戶,對同一個微信開放平台下的不同應用,unionid
是相同的。
加鎖的登錄
在某些情況下,我們或許多個地方會同時觸發登錄邏輯(如多個接口同時拉取,發現登錄態過期的情況)。一般來說,我們會簡單地給請求加個鎖來解決:
- 使用
isLogining
來標志是否請求中。 - 方法返回 Promise,登錄態過期時靜默續期后重新發起。
- 使用
sessionId
來記錄業務側的登錄態。
// session 參數 key(后台吐回) export const SESSION_KEY = 'sessionId'; let isLogining = false; export function doLogin() { return new Promise((resolve, reject) => { const session = wx.getStorageSync(SESSION_KEY); if (session) { // 緩存中有 session resolve(); } else if (isLogining) { // 正在登錄中,請求輪詢稍后,避免重復調用登錄接口 setTimeout(() => { doLogin() .then(res => { resolve(res); }) .catch(err => { reject(err); }); }, 500); } else { isLogining = true; wx.login({ success: (res) => { if (res.code) { const reqData: ILoginRequest = { code: res.code } wx.request({ url: API.login, data: reqData, // method: "POST", success: (resp) => { const data = resp.data; isLogining = false; // 保存登錄態 if (data.return_code === 0) { wx.setStorageSync(SESSION_KEY, data[SESSION_KEY]); resolve(); } else { reject(data.return_msg); } }, fail: err => { // 登錄失敗,解除鎖,防止死鎖 isLogining = false; reject(err); } }); } else { // 登錄失敗,解除鎖,防止死鎖 isLogining = false; reject(); } }, fail: (err) => { // 登錄失敗,解除鎖,防止死鎖 isLogining = false; reject(err); } }); } }); }
登錄態靜默續期的實現
checkSession
前面也提到,微信不會把session_key
的有效期告知開發者,因此需要使用接口wx.checkSession()
來校驗session_key
是否有效。
這里我們:
- 使用
isCheckingSession
來標志是否查詢中。 - 返回 Promise。
- 使用
isSessionFresh
來標志session_key
是否有效。
import {doLogin} from "./doLogin"; import {SESSION_KEY} from "./doLogin"; let isCheckingSession = false; let isSessionFresh = false; export function checkSession(): Promise<string> { return new Promise((resolve, reject) => { const session = wx.getStorageSync(SESSION_KEY); if (isCheckingSession) { setTimeout(() => { checkSession().then(res => { resolve(res); }).catch(err => { reject(err); }); }, 500); } else if (!isSessionFresh && session) { isCheckingSession = true; wx.checkSession({ success: () => { // session_key 未過期,並且在本生命周期一直有效 isSessionFresh = true; resolve(); }, fail: () => { // session_key 已經失效,需要重新執行登錄流程 wx.removeStorage({ key: "skey", complete: () => { doLogin().then(() => { resolve(); }).catch(err => { reject(err); }); } }); }, complete: () => { isCheckingSession = false; } }); } else { doLogin().then(res => { resolve(res); }).catch(err => { reject(err); }); } }); } } }
靜默續期的接口請求
至此,我們可以封裝一個簡單的接口,來在每次登錄態過期的時候自動續期:
- 在請求前,使用
checkSession()
檢車本次周期內session_key
是否有效,無效則doLogin()
拉起登錄獲取sessionId
。 - 請求接口,若返回特定登錄態失效錯誤碼(此處假設為
LOGIN_FAIL_CODE
),則doLogin()
拉起登錄獲取sessionId
。 - 使用
tryLoginCount
來標志重試次數,TRY_LOGIN_LIMIT
來標志重試次數上限,避免進入死循環。
import {doLogin} from "./doLogin"; import {SESSION_KEY} from "./doLogin"; import {checkSession} from "./checkSession"; // 會話過期錯誤碼,需要重新登錄 export const LOGIN_FAIL_CODES = [10000]; const TRY_LOGIN_LIMIT = 3; export function request(obj: any = {}): Promise<object> { return new Promise((resolve, reject) => { checkSession().then(() => { let session = wx.getStorageSync(SESSION_KEY); const {url, data, method, header, dataType} = obj; let tryLoginCount = obj.tryLoginCount || 0; // 如果需要通過 data 把登錄態 sessionId 帶上const dataWithSession = {...data, [SESSION_KEY]: session, appid: APPID}; wx.request({ url, data: dataWithSession, method, header, dataType, success: (res: any) => { if (res.statusCode === 200) { const data: ICommonResponse = res.data; // 登陸態失效特定錯誤碼判斷,且重試次數未達到上限 if (LOGIN_FAIL_CODES.indexOf(data.return_code) > -1 && tryLoginCount < TRY_LOGIN_LIMIT) { doLogin().then(() => { obj.tryLoginCount = ++tryLoginCount; request(obj).then(res => { resolve(res); }).catch(err => { reject(err); }); }); } else { resolve(res); } } else { reject(res); } }, fail: function (err) { reject(err); } }); }).catch(err => { reject(err); }); }); }
至此,我們大概包裝了一個能自動登錄或是進行靜默續期的一個請求接口。
參考
結束語
小程序的登錄和登錄態管理,大概是大部分小程序都需要的能力。code
和session_key
的設計,做了哪些事情來保護用戶的數據。
如何在全局范圍地保證登錄態的有效性,微信側的登錄態也好,業務側的登錄態也好,靜默續期的能力能給用戶帶來不少的體驗提升。
查看Github有更多內容噢:https://github.com/godbasin
本文轉載自:https://godbasin.github.io,如有侵權,請聯系作者。
【PHPer技術棧】專注后端開發,倡導開源文化,做一個好玩、有趣、有靈魂的PHPer工程師,歡迎大家關注!