小程序用戶登錄架構設計

上一篇文章《小程序靜默登錄方案設計》提到過,小程序可以通過微信官方提供的登錄能力方便地獲取微信提供的用戶身份標識,快速建立小程序內的用戶體系。

即「靜默登錄」,通過調用 wx.login 獲取到 code ,將其發送到開發者後端,開發者後端通過接口去微信後端換取到 openidsessionKey(現在會將 unionid 也一併返回)後,然後把自定義登錄態 3rd_session(本業務命名爲auth-token) 返回給前端,就已經完成登錄行爲了。

理論上,開發者後端可以通過 openid識別用戶,也能通過unionid關聯同主體的多個小程序、公衆號、app,實現數據互通,從而爲每一個用戶創建獨一無二的uid(本業務自定義的用戶 id),在「微信生態」中建立成熟用戶體系。

然而,對於複雜的電商跨端應用,比如pch5小程序,不同渠道註冊的uid是不同的,用戶登錄後難以對各個渠道的交易、促銷、收藏等數據進行整合。因此,要實現跨端的用戶體系數據互通,就需要提供一個唯一的用戶標識——手機號。這便是本文重點講述的「用戶登錄」,即「遊客態」轉變成「會員態」的過程。

上一篇文章《小程序靜默登錄方案設計》中提過,當新用戶第一次進入小程序時,便會觸發「靜默登錄」,這個過程對用戶是無感知的。但此時開發者服務端已經爲該用戶定義了uid,並下發auth-token給小程序端,對於一些需要鑑權的請求,服務端可以根據請求攜帶的auth-token精確識別是哪個用戶發起的行爲。

然而,類似加購下單領券等用戶行爲,涉及到跨端數據的整合,在執行用戶操作之前,會判斷用戶是否登錄,如若用戶未登錄,則跳轉登錄頁面,整個流程如下所示:

比如在「用戶中心」頁面點擊「我的訂單」,由於此時用戶未登錄,跳轉到登錄頁面,可以選擇以下兩種登錄方式:

  1. 選擇 「微信授權登錄」,彈出授權手機號信息彈窗,點擊「允許」,此時用戶登錄成功。
  2. 選擇 「手機快捷登錄」,輸入手機號,使用 「驗證碼」 或者 「密碼」 進行登錄,登錄成功跳轉回到「用戶中心」頁面。

上述步驟已經完成了「用戶登錄」,用戶可以正常的執行加購、領券、下單等操作。 爲了提升用戶體驗,需要對 「會員信息」 進行維護 ,比如暱稱、頭像、性別、生日等信息,最簡單的方法是 獲取「微信授權用戶信息」。觸發時機分爲以下兩種:

  1. 用戶第一次選擇 「微信授權登錄」 成功後跳轉授權用戶信息頁面,點擊 「授權用戶信息」,彈出授權用戶信息彈窗。點擊「允許」,跳轉回「用戶中心」頁面。
  2. 在「用戶中心」頁面點擊頭像暱稱區域,彈出授權用戶信息彈窗,點擊「允許」,更新「會員信息」並跳轉用戶信息編輯頁面。

3.1 架構

「用戶登錄」方案架構如上圖所示,將所有登錄相關功能抽象到 「service 層」(本項目將其命名爲session),供 「業務層」 調用。該 「service 層」 主要分爲以下兩個模塊:

3.1.1 libs - 提供登錄相關的類方法供「業務層」調用

  1. 封裝session類,提供類方法供「業務層」調用。主要有以下幾種方法:

jtz2XR

當然,session類中還封裝了一些方法用於與storage交互,比如獲取storage中的auth-token用於各種鑑權請求攜帶等等。session類也提供的一些拓展方法,比如註銷賬號、解綁手機號等等用於後續需求迭代。

  1. 裝飾器:

    • must-authmustAuth類方法的裝飾器,便於業務層各種場景觸發登錄。
    • fuse-line熔斷機制,如果短時間內多次調用,則停止響應一段時間,類似於 TCP 慢啓動。用於解決refreshLoginlogin等方法的併發處理問題。
    • single-queue單隊列模式,同一時間,只允許一個正在過程中的網絡請求。請求被鎖定之後,同樣的請求都會被推入隊列,等待進行中的請求返回後,消費同一個結果。用於解決refreshLoginlogin等方法的併發處理問題。

3.1.2 ui - 提供通用組件供業務層調用

  1. 基礎組件user-containerphone-container分別是獲取「微信授權用戶信息」和獲取「微信授權手機號」的純 UI 單元組件,給通用組件使用。
  2. behavior 類:拿到授權數據後需要發送給服務端進行存儲,也需要執行一些跳轉邏輯判斷,這些都抽象成行爲類封裝在auth-flow中,供通用組件使用。
  3. 通用組件: 共用一個行爲類,區別在於auth-flow-container用於頁面,auth-flow-popup用於彈窗。如下所示,小程序只有微信授權功能,則可以通過彈窗完成授權。如小程序同時提供手機號驗證碼和密碼登錄等功能,則需跳轉特定登錄頁面。

3.2 libs

3.2.1 用戶身份定義

綜上所示,用戶登錄的階段可以分爲以下三步:

export enum AuthStepType {
  
  ONE = 1,
  
  TWO = 2,
  
  THREE = 3,
}
複製代碼

那麼如何判斷用戶此時處於哪個步驟,基於「靜默登錄」的啓發,原本「靜默登錄」成功開發者後端會將自定義登錄態 auth-token返回給前端,此處請求可以攜帶返回「用戶信息」,同auth-token一起命名爲session存儲在本地storage當「用戶登錄」或者「更新用戶信息」時,會同步更新storagekeysession的數據,從而通過這些用戶數據判斷當前用戶處於哪一個登錄階段

以下表格列出了session存儲的部分重要的屬性以及在三個階段屬性對應的值。

91wc30

注意: 會員態和會員信息態的busiIdentity值均爲MEMBER,區分會員態和會員信息態可以通過用戶暱稱和頭像等字段,比如用戶登錄成功會爲用戶生成以'u_'開頭的默認暱稱和默認爲空的用戶頭像鏈接。

判斷用戶此時處於哪個步驟的代碼如下:

  
  public getCurrentAuthStep(): AuthStepType {
    
    const loginMode = this.getLoginMode();
    if (loginMode === LoginMode.SWITCH_ACCOUNT) return AuthStepType.ONE;

    
    const userInfo = this.getUser();
    if (userInfo?.busiIdentity !== 'MEMBER') return AuthStepType.ONE;

    
    if (userInfo.nickName.substring(0, 2) === 'u_' && !userInfo.headUrl)
      return AuthStepType.TWO;

    
    return AuthStepType.THREE;
  }
複製代碼

3.2.2 用戶登錄觸發場景

前面提到過,「用戶登錄」的 目的是爲了整合各個渠道的交易、促銷、收藏等數據,針對電商小程序,目前總結的需要用戶登錄的場景如下所示:

即當用戶登錄小程序時,可以正常瀏覽瀏覽商品,只有觸發某些特定行爲,比如領券、加購、收藏、下單等,纔會判斷用戶是否處於登錄狀態,如未登錄,跳轉登錄頁面

如下所示,封裝mustAuth方法進行攔截,未登錄則跳轉登錄頁面:

export default class Session {
  ...
  public mustAuth({
    mustAuthStep = AuthStepType.TWO, 
  } = {}): Promise<void> {
    
    if (this.getCurrentAuthStep() >= mustAuthStep) return Promise.resolve();
    
    Navigator.gotoPage('/login/home');
    
    return Promise.reject();
  }
}
複製代碼

上述代碼是跳轉頁面攔截,對於彈窗而言,需要把彈窗注入base-page(每個頁面都需要引入的通用組件,封裝每個頁面都需要使用的通用方法,比如錯誤處理等) 中,通過 id 查找到彈窗組件,並進行調用。

export default class Session {
  ...
   public mustAuth({
    mustAuthStep = AuthStepType.TWO, 
    popupCompName = 'auth-flow-popup',
  } = {}): Promise<void> {
    
    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);
      });
    };
  };
}
複製代碼

3.3 UI

3.3.1 基礎組件

1. phone-container 組件

因爲需要用戶主動觸發才能發起獲取微信授權手機號接口,需用 button 組件的點擊來觸發。組件代碼如下所示:

 <button open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber" hover-class="none" disabled="{{disabled}}"><slot></slot></button>


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開發者每次通過該接口獲取用戶個人信息均需用戶確認,因此需要妥善保管用戶授權的頭像暱稱,避免重複彈窗。

3.3.2 行爲類

如下圖所示,auth-flow行爲類主要封裝用戶、小程序、服務端三者之間的交互邏輯。

在「微信授權登錄」過程中,小程序拿到加密的encryptedDataiv數據,將其和攜帶的auth-token一起發送給開發者服務器,服務端通過auth-token鑑權識別這個用戶,並使用靜默登錄成功獲取的session_key(對稱解密密鑰)對encryptedDataiv數據進行對稱解密,獲取該用戶的手機號,將手機號與uid綁定,此時該用戶成功註冊會員,並將會員信息返回給小程序端。

小程序端更新本地storage存儲的session數據,此時busiIdentity的值已經從VISIT更新爲MEMBER,用戶身份轉變爲會員態,登錄成功。

在「授權用戶信息」的過程中,小程序調用wx.getUserProfile方法拿到用戶數據,並將這些數據與攜帶的auth-token一起發送給開發者服務器,服務端通過auth-token鑑權識別這個用戶,更新該用戶的信息並將新的會員數據返回給小程序端。

小程序端更新本地storage存儲的session數據,此時用戶暱稱和頭像均已更新,用戶身份轉變爲會員信息態,授權成功。

眼尖的讀者一定觀察到了,時序圖中還對微信頭像做了轉存。這是因爲用戶在微信端修改微信頭像後,之前「授權用戶信息」獲取的微信頭像鏈接就會失效,因此開發者應該在自己獲取用戶信息後,將頭像保存下來,避免微信頭像 URL 失效後的異常情況。

3.3.3 通用組件

通用組件是對基礎組件和行爲類的二次封裝,主要是爲業務層提供彈窗登錄和頁面登錄兩種能力。

  1. 總結

我們將用戶登錄能力從業務層中抽象出來,統一封裝在service層,便於複用。本文主要講述的是service層的架構,對於業務層的邏輯實現並沒有多加累贅。下列表格以小程序端爲例,簡述了「靜默登錄」和「用戶登錄」整套方案的前後端邏輯實現。

b8txvS

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://juejin.cn/post/6945264484491460638