1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        小程序靜默登錄方案設計

        共 12735字,需瀏覽 26分鐘

         ·

        2021-05-10 22:54

        點擊上方 程序員成長指北,關注公眾號

        回復1,加入Node進階交流群

        來源:蔡小真

        https://juejin.cn/post/6933082931653148680

        1. 背景

        首先談談在小程序的開發(fā)中,如何借助微信的能力標識一個用戶?

        微信官方提供了兩種標識:

        1. OpenId 是一個用戶對于一個小程序/公眾號的標識,開發(fā)者可以通過這個標識識別出用戶。
        2. UnionId 是一個用戶對于同主體微信小程序/公眾號/APP 的標識,開發(fā)者需要在微信開放平臺下綁定相同賬號的主體。開發(fā)者可通過UnionId,實現(xiàn)多個小程序、公眾號、甚至 APP 之間的數(shù)據(jù)互通。

        同一個用戶的這兩個 ID 對于同一個小程序來說是永久不變的,就算用戶刪了小程序,下次用戶進入小程序,開發(fā)者依舊可以通過后臺的記錄標識出來。那么如何獲取OpenIdUnionId呢?

        早期(2018 年 4 月之前)的小程序設計使用 wx.getUserInfo 接口,來獲取用戶信息。設計這個接口的初衷是希望開發(fā)者在真正需要用戶信息(如頭像、昵稱、手機號等)的情況下才去調取這個接口。但很多開發(fā)者為了拿到UnionId,會在小程序啟動時直接調用這個接口,導致用戶在使用小程序的時候產(chǎn)生困擾,歸結起來有幾點:

        1. 開發(fā)者在小程序首頁直接調用 wx.getUserInfo 進行授權,彈框獲取用戶信息,會使得一部分用戶點擊“拒絕”按鈕。
        2. 在開發(fā)者沒有處理用戶拒絕彈框的情況下,用戶必須授權頭像昵稱等信息才能繼續(xù)使用小程序,會導致某些用戶放棄使用該小程序。
        3. 用戶沒有很好的方式重新授權,盡管微信官方增加了設置頁面,可以讓用戶選擇重新授權,但很多用戶并不知道可以這么操作。

        微信官方也意識到了這個問題,針對獲取用戶信息更新了三個能力:

        1. 使用組件來獲取用戶信息。
        2. 若用戶滿足一定條件,則可以用wx.login 獲取到的 code 直接換到unionId
        3. wx.getUserInfo 不需要依賴 wx.login 就能調用得到數(shù)據(jù)。

        本文主要講述的是第二點能力,微信官方鼓勵開發(fā)者在不騷擾用戶的情況下合理獲得unionid,而僅在必要時才向用戶彈窗申請使用昵稱頭像,從而衍生出「靜默登錄」和「用戶登錄」兩種概念。

        2. 什么是靜默登錄?

        小程序可以通過微信官方提供的登錄能力方便地獲取微信提供的用戶身份標識,快速建立小程序內的用戶體系。

        很多開發(fā)者會把 wx.login 和 wx.getUserInfo 捆綁調用當成登錄使用,其實 wx.login 已經(jīng)可以完成登錄,wx.getUserInfo 只是獲取額外的用戶信息。

        在 wx.login 獲取到 code 后,會發(fā)送到開發(fā)者后端,開發(fā)者后端通過接口去微信后端換取到 openid 和 sessionKey(現(xiàn)在會將 unionid 也一并返回)后,把自定義登錄態(tài) 3rd_session(本業(yè)務命名為auth-token) 返回給前端,就已經(jīng)完成登錄行為了。wx.login 行為是靜默,不必授權的,用戶不會察覺。

        wx.getUserInfo 只是為了提供更優(yōu)質的服務而存在,比如獲取用戶的手機號注冊會員,或者展示頭像昵稱,判斷性別,開發(fā)者可通過 unionId 和其他公眾號上已有的用戶畫像結合來提供歷史數(shù)據(jù)。因此開發(fā)者不必在用戶剛剛進入小程序的時候就強制要求授權。

        2.1 靜默登錄流程時序

        官方給出了 wx.login 的最佳實踐如下:

        靜默登錄英文簡稱為silentLogin,代碼如下所示:

          private async silentLogin(): Promise<void> {
            try {
              this.status.silentLogin.ing();

              // 獲取臨時登錄憑證code
              const code = await getWxLoginCode();
              // 將code發(fā)送給服務端
              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;
            }
          }
        復制代碼

        總結為以下三步:

        1. 小程序端調用 wx.login() 獲取 臨時登錄憑證code ,并回傳到開發(fā)者服務器。
        2. 服務器端調用 auth.code2Session 接口,換取 用戶唯一標識 OpenID 和 會話密鑰 session_key。
        3. 開發(fā)者服務器可以根據(jù)用戶標識來生成自定義登錄態(tài)(例如:auth-token),用于后續(xù)業(yè)務邏輯中前后端交互時識別用戶身份。

        2.2 開發(fā)者后臺校驗與解密開放數(shù)據(jù)

        靜默登錄成功后,微信服務器端會下發(fā)一個session_key給服務端,而這個會在需要獲取微信開放數(shù)據(jù)的時候會用到。

        為了確保開放接口返回用戶數(shù)據(jù)的安全性,微信會對明文數(shù)據(jù)進行簽名。開發(fā)者可以根據(jù)業(yè)務需要對數(shù)據(jù)包進行簽名校驗,確保數(shù)據(jù)的完整性。

        1. 小程序通過調用接口(如 wx.getUserInfo)獲取數(shù)據(jù)時,如果用戶已經(jīng)授權,接口會同時返回以下幾個字段。如用戶未授權,會先彈出用戶彈窗,用戶點擊同意授權,接口會同時返回以下幾個字段。相反如果用戶拒絕授權,將調用失敗。
        屬性類型說明
        userInfoUserInfo用戶信息對象,不包含 openid 等敏感信息
        rawDatastring不包括敏感信息的原始數(shù)據(jù)字符串,用于計算簽名
        signaturestring使用 sha1( rawData + sessionkey ) 得到字符串,用于校驗用戶信息
        encryptedDatastring包括敏感數(shù)據(jù)在內的完整用戶信息的加密數(shù)據(jù)
        ivstring加密算法的初始向量
        cloudIDstring敏感數(shù)據(jù)對應的云 ID,開通云開發(fā)的小程序才會返回,可通過云調用直接獲取開放數(shù)據(jù)
        1. 開發(fā)者將 signature、rawData 發(fā)送到開發(fā)者服務器進行校驗。服務器利用用戶對應的 session_key 使用相同的算法計算出簽名 signature2 ,比對 signature 與 signature2 即可校驗數(shù)據(jù)的完整性。開發(fā)者服務器告訴前端開發(fā)者數(shù)據(jù)可信,即可安全使用用戶信息數(shù)據(jù)。
        2. 如果開發(fā)者想要獲取敏感數(shù)據(jù)(如 openid,unionID),則將encryptedDataiv發(fā)送到開發(fā)者服務器,由服務器使用session_key(對稱解密密鑰)進行對稱解密,獲取敏感數(shù)據(jù)進行存儲并返回給前端開發(fā)者。

        注意: 因為需要用戶主動觸發(fā)才能發(fā)起獲取手機號接口,所以該功能不由 API 來調用(即上述提到的wx.getUserInfo是無法獲取手機號的),需用 button 組件的點擊來觸發(fā)。獲得encryptedDataiv,同樣發(fā)送給開發(fā)者服務器,由服務器使用session_key(對稱解密密鑰)進行對稱解密,獲得對應的手機號。

        需要關注的是,2021 年 2 月 23 日,微信團隊發(fā)布了《小程序登錄、用戶信息相關接口調整說明》,進行了如下調整:

        1. 2021 年 2 月 23 日起,通過wx.login接口獲取的登錄憑證可直接換取unionID。
        2. 2021 年 4 月 13 日后發(fā)布新版本的小程序,無法通過wx.getUserInfo接口獲取用戶個人信息(頭像、昵稱、性別與地區(qū)),將直接獲取匿名數(shù)據(jù)。getUserInfo接口獲取加密后的openIDunionID數(shù)據(jù)的能力不做調整。
        3. 新增getUserProfile接口(基礎庫 2.10.4 版本開始支持),可獲取用戶頭像、昵稱、性別及地區(qū)信息,開發(fā)者每次通過該接口獲取用戶個人信息均需用戶確認。

        即開發(fā)者通過組件調用wx.getUserInfo將不再彈出彈窗,直接返回匿名的用戶個人信息。如果要獲取用戶頭像、昵稱、性別及地區(qū)信息,需要改造wx.getUserProfile接口。

        2.3 session_key 的有效期

        開發(fā)者如果遇到因為 session_key 不正確而校驗簽名失敗或解密失敗,請關注下面幾個與 session_key 有關的注意事項。

        1. wx.login 調用時,用戶的 session_key 可能會被更新而致使舊 session_key 失效(刷新機制存在最短周期,如果同一個用戶短時間內多次調用 wx.login,并非每次調用都導致 session_key 刷新)。開發(fā)者應該在明確需要重新登錄時才調用 wx.login,及時通過 auth.code2Session 接口更新服務器存儲的 session_key。
        2. 微信不會把 session_key 的有效期告知開發(fā)者。我們會根據(jù)用戶使用小程序的行為對 session_key 進行續(xù)期。用戶越頻繁使用小程序,session_key 有效期越長。
        3. 開發(fā)者在 session_key 失效時,可以通過重新執(zhí)行登錄流程獲取有效的 session_key。使用接口wx.checkSession可以校驗 session_key 是否有效,從而避免小程序反復執(zhí)行登錄流程。
        4. 當開發(fā)者在實現(xiàn)自定義登錄態(tài)時,可以考慮以 session_key 有效期作為自身登錄態(tài)有效期,也可以實現(xiàn)自定義的時效性策略。

        3 「登錄」架構

        用戶登錄架構

        「登錄」方案架構如上圖所示,將所有登錄相關功能抽象到 「service 層」(本項目將其命名為session),供 「業(yè)務層」 調用。本文主要講述灰色內容,其它模塊將在下一篇文章《小程序用戶登錄設計》中闡述。

        3.1 libs - 提供登錄相關的類方法供「業(yè)務層」調用

        1. 封裝session類,提供類方法供「業(yè)務層」調用。主要有以下幾種方法:
        方法名功能使用場景
        silentLogin發(fā)起靜默登錄-
        login登錄,silentLogin 方法的一層封裝用于小程序啟動時發(fā)起靜默登錄
        refreshLogin刷新登錄態(tài),silentLogin 方法的一層封裝用于登錄態(tài)過期時發(fā)起靜默登錄
        ensureSessionKey驗證 sessionKey 是否過期,過期則刷新登錄態(tài)綁定微信授權手機號時驗證是否過期,過期則得重新彈窗授權
        1. 裝飾器:

          • fuse-line:熔斷機制,如果短時間內多次調用,則停止響應一段時間,類似于 TCP 慢啟動。用于解決refreshLogin、login等方法的并發(fā)處理問題。
          • single-queue:單隊列模式,同一時間,只允許一個正在過程中的網(wǎng)絡請求。請求被鎖定之后,同樣的請求都會被推入隊列,等待進行中的請求返回后,消費同一個結果。用于解決refreshLogin、login等方法的并發(fā)處理問題。

        4. 靜默登錄的調用時機

        4.1 小程序啟動時調用

        由于大部分情況都需要依賴登錄態(tài),在小程序啟動的時候(app.onLaunch())調用靜默登錄是最常見的手段。這里我們封裝一個login函數(shù)如下所示,首先調用wx.checkSession判斷session_key是否過期,如果session_key未過期且本地存在auth_token自定義登錄態(tài),表示當前的靜默登錄態(tài)仍然有效,無需進行其它操作。否則,表示靜默登錄態(tài)失效或者新用戶從未發(fā)起過靜默登錄,那么發(fā)起靜默登錄流程。

        public async login(): Promise<void> {
            // 調用wx.checkSession判斷session_key是否過期
            const hasSession = await checkSession();

            // 本地已有可用登錄態(tài)且session_key未過期,resolve。
            if (this.getAuthToken() && hasSession) return Promise.resolve();

            // 否則,發(fā)起靜默登錄
            await this.silentLogin();
        }
        復制代碼

        但是由于原生的小程序啟動流程中, App,Page,Component 的生命周期鉤子函數(shù),都不支持異步阻塞。所以很有可能出現(xiàn)小程序頁面加載完成后,靜默登錄過程還沒有執(zhí)行完畢的情況,這會導致后續(xù)一些依賴登錄態(tài)的操作(比如請求發(fā)起)出錯

        4.2 接口請求發(fā)起時調用

        保險起見,如果某些接口需要攜帶自定義登錄態(tài)進行鑒權,則需要在請求發(fā)起時進行攔截,校驗登錄態(tài),并刷新登錄。刷新登錄代碼如下所示:

          public async refreshLogin(): Promise<void> {
            try {
              // 清除 Session
              this.clearSession();
              // 發(fā)起靜默登錄
              await this.silentLogin();
            } catch (error) {
              throw error;
            }
          }
        復制代碼

        整個流程如下圖所示:

        • 攔截 request
          1. 判斷是否需要鑒權:請求發(fā)起時,攔截請求,判斷請求是否需要添加auth-token,如若不需要,直接發(fā)起請求。如若需要,執(zhí)行第二步。
          2. 判斷是否需要發(fā)起靜默登錄:判斷 storage 中是否存在auth-token,如若不存在,發(fā)起「刷新登錄」。
          3. 請求頭部添加auth-token:添加auth-token,發(fā)起請求。
        • 與服務端通信:發(fā)起請求,服務端處理請求返回結果。
        • 攔截 response: 解析狀態(tài)碼
          1. 狀態(tài)碼為AUTH_FAIL:服務端返回code為“鑒權失敗”,觸發(fā)這種情景的原因有兩個,一是接口需要鑒權,但是發(fā)起請求時未攜帶auth-token,二是auth-token過期。這時將上一次請求攜帶的auth-token與本地存儲的auth-token比較,如果不一致,表示登錄態(tài)已經(jīng)刷新過了,那么就直接重新發(fā)起請求。如果一致,發(fā)起刷新登錄,拿到新的auth-token后重新發(fā)起請求,這個動作對用戶來說是無感知的。
          2. 狀態(tài)碼為USER_WX_SESSIONKEY_EXPIRE:服務器返回code為“用戶登錄態(tài)過期”,這是針對用戶授權手機號登錄失敗定制的狀態(tài)碼,如果登錄態(tài)已過期,表示存儲在服務端的session_key也是過期的,那么點擊授權手機號獲取的加密數(shù)據(jù)發(fā)送到服務端進行對稱解密,由于session_key失效,無法解密出真正的手機號。因此需要重新發(fā)起靜默登錄,等待用戶重新點擊授權按鈕獲取新的加密數(shù)據(jù),然后發(fā)起新的解密請求
          3. 狀態(tài)碼為其它:比如Success或者其他業(yè)務請求錯誤的情況,不進行攔截,返回 response 讓業(yè)務代碼解析。

        4.3 wx.checkSession 罷工之謎

        基于上述接口請求發(fā)起時調用的流程,很多人會有疑問,既然服務端會返回auth-token過期的狀態(tài)碼,為啥不在請求發(fā)送前進行攔截,使用wx.checkSession接口校驗登錄態(tài)是否過期(如下圖所示,增加紅框內的步驟)?

        這是因為,我們通過實驗發(fā)現(xiàn),在 session_key 已過期的情況下,wx.checkSession 有一定的幾率返回true。即增加wx.checkSession步驟并不能百分百保證登錄態(tài)不會過期,后續(xù)仍然需要對不同的狀態(tài)碼進行處理。

        社區(qū)也有相關的反饋未得到解決:

        • 小程序解密手機號,隔一小段時間后,checksession:ok,但是解密失敗
        • wx.checkSession 有效,但是解密數(shù)據(jù)失敗
        • checkSession 判斷 session_key 未失效,但是解密手機號失敗

        所以結論是:wx.checkSession可靠性是不達 100% 的。

        基于以上,我們需要對 session_key 的過期做一些容錯處理:

        1. 發(fā)起需要使用 session_key 的請求前,做一次 wx.checkSession 操作,如果失敗了刷新登錄態(tài)。
        2. 后端使用session_key解密開放數(shù)據(jù)失敗之后,返回特定錯誤碼(如:USER_WX_SESSIONKEY_EXPIRE),前端刷新登錄態(tài)。

        4.4 并發(fā)處理

        我們知道,當啟動小程序時,各種監(jiān)控、埋點數(shù)據(jù)上報都需要獲取用戶的個人信息,這些信息都得「靜默登錄」后才能獲取,因此會同時發(fā)起多個login請求。另一種情況下,假設一個新用戶進入一個業(yè)務復雜的頁面,同時發(fā)起五個不同的業(yè)務請求,恰巧這五個請求都需要鑒權,那么五個請求都會被攔截并發(fā)起refreshLogin請求。顯然,這樣的并發(fā)是不合理的。

        基于此,我們設計了如下方案:

        • 單隊列模式

          1. 請求鎖:同一時間,只允許一個正在過程中的網(wǎng)絡請求。

          2. 等待隊列:請求被鎖定之后,同樣的請求都會被推入隊列,等待進行中的請求返回后,消費同一個結果。

        • 熔斷機制:如果短時間內多次調用,則停止響應一段時間,類似于 TCP 慢啟動。

        如上圖所示,首先refreshLogin請求入隊,隊列中只有一個請求,發(fā)送該請求,同時保險絲計入次數(shù) 1,服務端返回請求結果,消費結果。接著又發(fā)起一個refreshLogin請求,隊列中只有一個請求,發(fā)送該請求,同時保險絲計入次數(shù) 2。然后又連續(xù)發(fā)起三個請求,由于上一個請求還沒有執(zhí)行完成,將這三個請求入隊,等待上一個請求結果返回,隊列中的四個請求消費同一個結果。由于觸發(fā)自動冷卻閾值,保險絲重置。

        以上兩種方案通過裝飾器模式引入,代碼如下所示,refreshLogin函數(shù)其實是slientLogin函數(shù)的一層封裝,用于接口發(fā)起時調用。而前面提到的login函數(shù)也是slientLogin函數(shù)的一層封裝,用戶小程序啟動時調用。

          @singleQueue({ name'refreshLogin' })
          @fuseLine({ name'refreshLogin' })
          public async refreshLogin(): Promise<void> {
            try {
              // 清除 Session
              this.clearSession();
              await this.silentLogin();
            } catch (error) {
              throw error;
            }
          }
        復制代碼

        到此,很多讀者可能對熔斷機制還不甚理解,熔斷的目的是為一個函數(shù)提供保險絲保障,短時間內多次調用,會熔斷一段時間,這段時間內拒絕所有請求。如果在自動冷卻閾值內,沒有請求通過,則重置保險絲。代碼如下所示:

        export default function fuseLine({
          // 一次熔斷前重試次數(shù)
          tryTimes = 3,

          // 重試間隔,單位 ms
          restoreTime = 5000,

          // 自動冷卻閾值,單位 ms
          coolDownThreshold = 1000,

          // 名稱
          name = 'unnamed',
        }: {
          tryTimes?: number;
          restoreTime?: number;
          name?: string;
          coolDownThreshold?: number;
        } = {}
        {
          // 請求鎖
          let fuseLocked = false;

          // 當前重試次數(shù)
          let fuseTryTimes = tryTimes;

          // 自動冷卻
          let coolDownTimer;

          // 重置保險絲
          const reset = () => {
            fuseLocked = false;
            fuseTryTimes = tryTimes;
            logger.info(`${name}-保險絲重置`);
          };

          const request = async () => {
            if (fuseLocked) throw new Error(`${name}-保險絲已熔斷,請稍后重試`);

            // 已達最大重試次數(shù)
            if (fuseTryTimes <= 0) {
              fuseLocked = true;

              // 重置保險絲
              setTimeout(() => reset(), restoreTime);

              throw new Error(`${name}-保險絲熔斷!!`);
            }

            // 自動冷卻系統(tǒng)
            if (coolDownTimer) clearTimeout(coolDownTimer);
            coolDownTimer = setTimeout(() => reset(), coolDownThreshold);

            // 允許當前請求通過保險絲,記錄 +1
            fuseTryTimes = fuseTryTimes - 1;
            logger.info(`${name}-通過保險絲(${tryTimes - fuseTryTimes}/${tryTimes})`);
            return Promise.resolve();
          };

          return function(
            _target: Record<string, any>,
            _propertyName: string,
            descriptor: TypedPropertyDescriptor<(...args: any[]
        ) => any>,
          ) 
        {
            const method = descriptor.value;
            descriptor.value = async function(...args: any[]{
              await request();
              if (method) return method.apply(this, args);
            };
          };
        }
        復制代碼

        5. 最后

        讀到這里,相信你已經(jīng)了解「靜默登錄」和「用戶登錄」的區(qū)別?!胳o默登錄」是獲取微信登錄態(tài)的過程,通過獲取微信提供的用戶身份標識,快速建立小程序內的用戶體系?!赣脩舻卿洝故怯脩羰跈鄠€人開放數(shù)據(jù)成為會員的過程,是指從游客態(tài)轉換成會員態(tài)的,擁有購買等操作權限。

        兩者并不是一個概念,「用戶登錄」會在下一篇文章《小程序用戶登錄架構設計》中進行闡述。

        如果覺得這篇文章還不錯
        點擊下面卡片關注我
        來個【分享、點贊、在看】三連支持一下吧

           “分享、點贊、在看” 支持一波  

        瀏覽 38
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            午夜日皮 | 小婷的性泛滥日记h | 五月丁香婷婷啪 | 日本免费版网站nba | 中文字幕av久久爽 | 日韩爱爱网站 | 婷婷国产在线 | 国产a免费观看 | 一级a看片在线观看 | www.乱 |