上一篇文章《小程序靜默登陸方案設計》提到過,小程序能夠經過微信官方提供的登陸能力方便地獲取微信提供的用戶身份標識,快速創建小程序內的用戶體系。前端
即「靜默登陸」,經過調用 wx.login
獲取到 code
,將其發送到開發者後端,開發者後端經過接口去微信後端換取到 openid
和 sessionKey
(如今會將 unionid
也一併返回)後,而後把自定義登陸態 3rd_session
(本業務命名爲auth-token
) 返回給前端,就已經完成登陸行爲了。小程序
理論上,開發者後端能夠經過 openid
識別用戶,也能經過unionid
關聯同主體的多個小程序、公衆號、app,實現數據互通,從而爲每個用戶建立獨一無二的uid
(本業務自定義的用戶 id),在「微信生態」中創建成熟用戶體系。後端
然而,對於複雜的電商跨端應用,好比pc
、h5
、小程序
,不一樣渠道註冊的uid
是不一樣的,用戶登陸後難以對各個渠道的交易、促銷、收藏等數據進行整合。所以,要實現跨端的用戶體系數據互通,就須要提供一個惟一的用戶標識——手機號。這即是本文重點講述的「用戶登陸」,即「遊客態」轉變成「會員態」的過程。微信小程序
上一篇文章《小程序靜默登陸方案設計》中提過,當新用戶第一次進入小程序時,便會觸發「靜默登陸」,這個過程對用戶是無感知的。但此時開發者服務端已經爲該用戶定義了uid
,並下發auth-token
給小程序端,對於一些須要鑑權的請求,服務端能夠根據請求攜帶的auth-token
精確識別是哪一個用戶發起的行爲。緩存
然而,相似加購
、下單
、領券
等用戶行爲,涉及到跨端數據的整合,在執行用戶操做以前,會判斷用戶是否登陸,如若用戶未登陸,則跳轉登陸頁面,整個流程以下所示:服務器
好比在「用戶中心」頁面點擊「個人訂單」,因爲此時用戶未登陸,跳轉到登陸頁面,能夠選擇如下兩種登陸方式:微信
上述步驟已經完成了「用戶登陸」,用戶能夠正常的執行加購、領券、下單等操做。 爲了提高用戶體驗,須要對 「會員信息」 進行維護 ,好比暱稱、頭像、性別、生日等信息,最簡單的方法是 獲取「微信受權用戶信息」。觸發時機分爲如下兩種:markdown
「用戶登陸」方案架構如上圖所示,將全部登陸相關功能抽象到 「service 層」(本項目將其命名爲session
),供 「業務層」 調用。該 「service 層」 主要分爲如下兩個模塊:網絡
libs
- 提供登陸相關的類方法供「業務層」調用session
類,提供類方法供「業務層」調用。主要有如下幾種方法:方法名 | 功能 | 使用場景 |
---|---|---|
silentLogin |
發起靜默登陸 | - |
login |
登陸,silentLogin 方法的一層封裝 |
用於小程序啓動時發起靜默登陸 |
refreshLogin |
刷新登陸態,silentLogin 方法的一層封裝 |
用於登陸態過時時發起靜默登陸 |
ensureSessionKey |
驗證sessionKey 是否過時,過時則刷新登陸態 |
綁定微信受權手機號時驗證是否過時,過時則得從新彈窗受權 |
bindPhone |
綁定微信受權手機號 | 微信受權手機號彈窗點擊「容許」觸發 |
updateUser |
綁定微信受權用戶信息 | 微信受權用戶信息點擊「容許」觸發 |
getCurrentAuthStep |
獲取當前用戶登陸所屬階段 | 詳見下文 |
mustAuth |
各類觸發場景攔截判斷是否須要登陸 | 詳見下文 |
固然,session
類中還封裝了一些方法用於與storage
交互,好比獲取storage
中的auth-token
用於各類鑑權請求攜帶等等。session
類也提供的一些拓展方法,好比註銷帳號、解綁手機號等等用於後續需求迭代。session
裝飾器:
must-auth
: mustAuth
類方法的裝飾器,便於業務層各類場景觸發登陸。fuse-line
: 熔斷機制,若是短期內屢次調用,則中止響應一段時間,相似於 TCP 慢啓動。用於解決refreshLogin
、login
等方法的併發處理問題。single-queue
: 單隊列模式,同一時間,只容許一個正在過程當中的網絡請求。請求被鎖定以後,一樣的請求都會被推入隊列,等待進行中的請求返回後,消費同一個結果。用於解決refreshLogin
、login
等方法的併發處理問題。ui
- 提供通用組件供業務層調用user-container
和phone-container
分別是獲取「微信受權用戶信息」和獲取「微信受權手機號」的純 UI 單元組件,給通用組件使用。auth-flow
中,供通用組件使用。auth-flow-container
用於頁面,auth-flow-popup
用於彈窗。以下所示,小程序只有微信受權功能,則能夠經過彈窗完成受權。如小程序同時提供手機號驗證碼和密碼登陸等功能,則需跳轉特定登陸頁面。綜上所示,用戶登陸的階段能夠分爲如下三步:
// 用戶登陸的階段
export enum AuthStepType {
// 階段一:遊客態:靜默登陸成功,未綁定手機號,無用戶信息
ONE = 1,
// 階段二:會員態:用戶登陸成功,已綁定手機號,無用戶信息
TWO = 2,
// 階段三:會員信息態:用戶登陸成功,已綁定手機號,有用戶信息
THREE = 3,
}
複製代碼
那麼如何判斷用戶此時處於哪一個步驟,基於「靜默登陸」的啓發,本來「靜默登陸」成功開發者後端會將自定義登陸態 auth-token
返回給前端,此處請求能夠攜帶返回「用戶信息」,同auth-token
一塊兒命名爲session
存儲在本地storage
。當「用戶登陸」或者「更新用戶信息」時,會同步更新storage
中key
爲session
的數據,從而經過這些用戶數據判斷當前用戶處於哪個登陸階段。
如下表格列出了session
存儲的部分重要的屬性以及在三個階段屬性對應的值。
屬性 | 定義 | 遊客態 | 會員態 | 會員信息態 |
---|---|---|---|---|
authToken |
自定義登陸態 | '0d5bad172...' | '0d5bad172...' | '0d5bad172...' |
uid |
用戶 id | '001' | '001' | '001' |
busiIdentity |
用戶身份定義 | 'VISIT' | 'MEMBER' | 'MEMBER' |
nickName |
用戶暱稱 | '' | 'u_a1bk45' | 'rileycai' |
headUrl |
頭像連接 | '' | '' | 'www.xx.com/image/...' |
phone |
手機號碼 | '' | '17600888888' | '17600888888' |
... | 其它用戶信息 | ... | ... | ... |
注意: 會員態和會員信息態的busiIdentity
值均爲MEMBER
,區分會員態和會員信息態能夠經過用戶暱稱和頭像等字段,好比用戶登陸成功會爲用戶生成以'u_'開頭的默認暱稱和默認爲空的用戶頭像連接。
判斷用戶此時處於哪一個步驟的代碼以下:
// 獲取當前受權階段
public getCurrentAuthStep(): AuthStepType {
// 切換帳號登陸的時候,始終返回AuthStepType.ONE
const loginMode = this.getLoginMode();
if (loginMode === LoginMode.SWITCH_ACCOUNT) return AuthStepType.ONE;
// 用戶身份定義非會員返回AuthStepType.ONE
const userInfo = this.getUser();
if (userInfo?.busiIdentity !== 'MEMBER') return AuthStepType.ONE;
// 初次登陸,未受權用戶信息,返回AuthStepType.TWO
if (userInfo.nickName.substring(0, 2) === 'u_' && !userInfo.headUrl)
return AuthStepType.TWO;
// 都有,返回AuthStepType.THREE
return AuthStepType.THREE;
}
複製代碼
前面提到過,「用戶登陸」的 目的是爲了整合各個渠道的交易、促銷、收藏等數據,針對電商小程序,目前總結的須要用戶登陸的場景以下所示:
即當用戶登陸小程序時,能夠正常瀏覽瀏覽商品,只有觸發某些特定行爲,好比領券、加購、收藏、下單等,纔會判斷用戶是否處於登陸狀態,如未登陸,跳轉登陸頁面。
以下所示,封裝mustAuth
方法進行攔截,未登陸則跳轉登陸頁面:
export default class Session {
...
public mustAuth({
mustAuthStep = AuthStepType.TWO, // 傳人蔘數,須要受權的LEVEL
} = {}): Promise<void> {
// 當前階段處於會員態(2)或者會員信息態(3),執行resolve操做
if (this.getCurrentAuthStep() >= mustAuthStep) return Promise.resolve();
// 當前階段處於遊客態(1),跳轉登陸頁
Navigator.gotoPage('/login/home');
// 執行reject操做
return Promise.reject();
}
}
複製代碼
上述代碼是跳轉頁面攔截,對於彈窗而言,須要把彈窗注入base-page
(每一個頁面都須要引入的通用組件,封裝每一個頁面都須要使用的通用方法,好比錯誤處理等)中,經過 id 查找到彈窗組件,並進行調用。
export default class Session {
...
public mustAuth({
mustAuthStep = AuthStepType.TWO, // 須要受權的LEVEL
popupCompName = 'auth-flow-popup',
} = {}): Promise<void> {
// 當前階段處於會員態(2)或者會員信息態(3),執行resolve操做
if (this.getCurrentAuthStep() >= mustAuthStep) return Promise.resolve();
// 獲取彈窗組件
const pages = getCurrentPages();
const curPage = pages[pages.length - 1];
const context = curPage.$$basePage || curPage;
const popupComp = context.selectComponent(`#${popupCompName}`);
// 容錯處理
if (!popupComp) {
return Promise.reject(
new Error(
"當前頁面未找到 #auth-popup 組件,請參考 'doc/登陸組件的使用方式.md'",
),
);
}
// 調用彈窗組件方法
popupComp.setMustAuthStep(mustAuthStep);
popupComp.nextStep();
// 等待受權成功回調
return this.waitAuth();
}
}
複製代碼
各個業務使用時能夠經過session.mustAuth().then(() => {...});
進行調用,爲了提升使用體驗,也可使用裝飾器@mustAuth()
來修飾各個業務需求 類的方法,裝飾器源碼以下:
/** * 登陸檢查裝飾器,使用該裝飾器的方法,會先執行受權檢查,若是未受權,將跳轉登陸頁面 */
export default function mustAuth(option = {}) {
return function( _target: Record<string, any>, _propertyName: string, descriptor: TypedPropertyDescriptor<(...args: any[]) => any>, ) {
const method = descriptor.value;
descriptor.value = function(...args: any[]) {
if (!session) return;
// 登陸攔截
return session.mustAuth(option).then(() => {
if (method) return method.apply(this, args);
});
};
};
}
複製代碼
1. phone-container 組件
由於須要用戶主動觸發才能發起獲取微信受權手機號接口,需用 button
組件的點擊來觸發。組件代碼以下所示:
// index.wxml
<button class="reset-button" open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber" hover-class="none" disabled="{{disabled}}"><slot></slot></button>
// index.ts
export default class PhoneContainer extends BaseComponent {
getPhoneNumber( e: WechatMiniprogram.Event<WechatMiniprogram.GetPhoneNumberCallbackResult>, ) {
this.triggerEvent('getphonenumber', { ...e.detail, authType: AuthType.PHONE,});
}
}
複製代碼
phone-container
是一個純 UI 組件,經過triggerEvent
事件將獲取手機號數據傳遞給父組件,
2. user-container 組件
user-container
組件是獲取微信受權用戶信息的純 UI 組件,以前經過<button open-type="getUserInfo" bindgetUserInfo="getUserInfo"/>
的方式進行獲取。2021 年 2 月 23 日,微信團隊發佈了《小程序登陸、用戶信息相關接口調整說明》,新增getUserProfile
接口替代原來的wx.getUserInfo
,來獲取用戶頭像、暱稱、性別及地區信息,也是經過button
組件的點擊來觸發。二者的區別以下圖所示:
2012 年 4 月 13 日以前,使用wx.getUserInfo
彈出受權彈窗時,若是用戶點擊容許受權,那麼會記錄用戶的行爲,下次再點擊時,不會彈窗而是直接將受權結果返回。4 月 13 日以後後,使用wx.getUserProfile
,開發者每次經過該接口獲取用戶我的信息均需用戶確認,所以須要妥善保管用戶受權的頭像暱稱,避免重複彈窗。
以下圖所示,auth-flow
行爲類主要封裝用戶、小程序、服務端三者之間的交互邏輯。
在「微信受權登陸」過程當中,小程序拿到加密的encryptedData
和iv
數據,將其和攜帶的auth-token
一塊兒發送給開發者服務器,服務端經過auth-token
鑑權識別這個用戶,並使用靜默登陸成功獲取的session_key
(對稱解密密鑰)對encryptedData
和iv
數據進行對稱解密,獲取該用戶的手機號,將手機號與uid
綁定,此時該用戶成功註冊會員,並將會員信息返回給小程序端。
小程序端更新本地storage
存儲的session
數據,此時busiIdentity
的值已經從VISIT
更新爲MEMBER
,用戶身份轉變爲會員態,登陸成功。
在「受權用戶信息」的過程當中,小程序調用wx.getUserProfile
方法拿到用戶數據,並將這些數據與攜帶的auth-token
一塊兒發送給開發者服務器,服務端經過auth-token
鑑權識別這個用戶,更新該用戶的信息並將新的會員數據返回給小程序端。
小程序端更新本地storage
存儲的session
數據,此時用戶暱稱和頭像均已更新,用戶身份轉變爲會員信息態,受權成功。
眼尖的讀者必定觀察到了,時序圖中還對微信頭像作了轉存。這是由於用戶在微信端修改微信頭像後,以前「受權用戶信息」獲取的微信頭像連接就會失效,所以開發者應該在本身獲取用戶信息後,將頭像保存下來,避免微信頭像 URL 失效後的異常狀況。
通用組件是對基礎組件和行爲類的二次封裝,主要是爲業務層提供彈窗登陸和頁面登陸兩種能力。
咱們將用戶登陸能力從業務層中抽象出來,統一封裝在service
層,便於複用。本文主要講述的是service
層的架構,對於業務層的邏輯實現並無多加累贅。下列表格以小程序端爲例,簡述了「靜默登陸」和「用戶登陸」整套方案的先後端邏輯實現。
業務場景 | 用戶感知 | 前端處理邏輯 | 後端處理邏輯 | 補充說明 |
---|---|---|---|---|
掃碼搜索等各類方式進入小程序 | 無 | 一、判斷:當前小程序是否緩存了登陸態auth-token 且使用wx.checkSeesion 檢查當前用戶在小程序中登陸態是否過時,過時執行步驟 2;二、使用 wx.login 獲取認證信息,請求後端wxLogin 接口獲取微信小程序認證默認綁定的用戶身份以及登陸態auth-token 。 |
一、解析微信加密信息獲取認證身份openid 和unionId ;二、查找 openid 是否已經綁定了對應的用戶,若綁定直接返回併爲其生成對應的登陸態auth-token ;三、新用戶會根據 openid 爲其自動生成一個用戶身份uid (見右補充說明)。 |
a、存在聚合根標識unionId && 有用戶信息:將已有聚合根用戶對應的exUid 直接映射到當前uid 下;b、存在聚合根標識 unionId && 無用戶信息:根據unionId 生成對應的帳號,但和opneid 對應的uid 一致;c、不存在聚合根標識:直接爲對應 openid 初始化一個uid 。 |
收藏、加購、下單、領券等操做 | 攔截跳轉 | 一、判斷: 當前用戶身份處於遊客態,跳轉登陸頁面。 | 對應域服務後端接口能夠根據請求攜帶的auth-token 進行鑑權,判斷用戶是否有操做權限 |
- |
用戶登陸 或者 切換帳號 | 選擇: 一、受權微信手機號登陸; 二、輸入手機號並使用驗證碼/密碼登陸 |
一、用戶選擇受權手機號登陸,後端會根據上一次靜默登陸的sesssionKey 解密,若是解密失敗須要從新走一遍靜默登陸後再讓客戶重試。二、用戶選擇經過驗證碼登陸時,需關注驗證碼時效和重試機制,並有錯誤處理邏輯; 三、用戶選擇密碼登陸時,後臺會返回帳戶未註冊或帳號密碼不對等錯誤,須要有獨立邏輯跳轉驗證碼註冊或找回密碼 四、以上三種方式都須要攜帶 auth-token 進行鑑權 |
一、根據auth-token 獲取當前的渠道基本認證帳戶openid -unionId -uid ;二、受權手機號登陸時須要先解密出手機號,此時不須要校驗,輸入手機號登陸時須要會走「密碼」或「驗證碼」校驗,密碼校驗會攔截帳號不存在或密碼錯誤的場景; 三、根據手機號判斷當前聚合根下是否存在對應的手機號渠道帳號(綁定流程見右補充說明)。 四、返回登陸結果。 |
a、手機號已存在:將已存在的用戶exUid 綁定至當前登陸態帳號;b、手機號不存在 && 用戶身份是遊客:將手機號和遊客對應的 uid 進行綁定c、手機號不存在 && 用戶身份是會員:爲手機號生成一個新的 newUid ,並將當前登陸的 openid 渠道帳戶綁定至該newUid 。 |
做者水平有限,敬請指教~