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>

        小程序靜默登錄方案設(shè)計

        共 12783字,需瀏覽 26分鐘

         ·

        2021-04-30 08:55

        點擊上方 前端瓶子君,關(guān)注公眾號

        回復算法,加入前端編程面試算法每日一題群


        來源:蔡小真

        https://juejin.cn/post/6933082931653148680

        1. 背景

        首先談?wù)勗谛〕绦虻拈_發(fā)中,如何借助微信的能力標識一個用戶?

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

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

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

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

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

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

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

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

        2. 什么是靜默登錄?

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

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

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

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

        2.1 靜默登錄流程時序

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

        9742b11fe3d0d00a2a3ffb5453533e9f.webp

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

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

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

        總結(jié)為以下三步:

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

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

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

        e6e247681008cb9b412e50811cd481f4.webp

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

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

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

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

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

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

        2.3 session_key 的有效期

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

        1. wx.login 調(diào)用時,用戶的 session_key 可能會被更新而致使舊 session_key 失效(刷新機制存在最短周期,如果同一個用戶短時間內(nèi)多次調(diào)用 wx.login,并非每次調(diào)用都導致 session_key 刷新)。開發(fā)者應該在明確需要重新登錄時才調(diào)用 wx.login,及時通過 auth.code2Session 接口更新服務(wù)器存儲的 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 「登錄」架構(gòu) 39bc2ace2ea59df6f94481837bd84905.webp用戶登錄架構(gòu)

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

        3.1 libs - 提供登錄相關(guān)的類方法供「業(yè)務(wù)層」調(diào)用

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

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

        4.1 小程序啟動時調(diào)用

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

        public async login(): Promise<void> {
            // 調(diào)用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ā)起時調(diào)用

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

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

        整個流程如下圖所示:

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

        4.3 wx.checkSession 罷工之謎

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

        27ed1d1254c32c699371eca7481d881f.webp

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

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

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

        所以結(jié)論是: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請求。另一種情況下,假設(shè)一個新用戶進入一個業(yè)務(wù)復雜的頁面,同時發(fā)起五個不同的業(yè)務(wù)請求,恰巧這五個請求都需要鑒權(quán),那么五個請求都會被攔截并發(fā)起refreshLogin請求。顯然,這樣的并發(fā)是不合理的。

        基于此,我們設(shè)計了如下方案:

        • 單隊列模式

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

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

        熔斷機制:如果短時間內(nèi)多次調(diào)用,則停止響應一段時間,類似于 TCP 慢啟動。

        7f9b8c1079aa7b47b60a233b38442309.webpa20f6b137fb04fcaab422d4757a3df10.webp

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

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

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

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

        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)的過程,通過獲取微信提供的用戶身份標識,快速建立小程序內(nèi)的用戶體系?!赣脩舻卿洝故怯脩羰跈?quán)個人開放數(shù)據(jù)成為會員的過程,是指從游客態(tài)轉(zhuǎn)換成會員態(tài)的,擁有購買等操作權(quán)限。

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

        最后

        歡迎關(guān)注【前端瓶子君】??ヽ(°▽°)ノ?
        回復「算法」,加入前端編程源碼算法群,每日一道面試題(工作日),第二天瓶子君都會很認真的解答喲!
        回復「交流」,吹吹水、聊聊技術(shù)、吐吐槽!回復「閱讀」,每日刷刷高質(zhì)量好文!如果這篇文章對你有幫助,在看」是最大的支持
        》》面試官也在看的算法資料《《
        “在看和轉(zhuǎn)發(fā)”就是最大的支持


        瀏覽 80
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

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

        手機掃一掃分享

        分享
        舉報
        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>
            蜜臀久久99精品久久久酒店 | 青青操在线观看 | 爱爱视频久久 | 免费婬乱AAA大片女人 | 最近中文字幕免费MV第一季歌词十 | 亚洲精品无码国产片不卡 | 少妇的呻吟声 | 操逼视频国产 | 国产丝袜一区二区三区免费视频 | 草逼无码 |