每一個有數據交互的小程序,都會涉及到登錄、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工程師,歡迎大家關注!

