小程序模板網(wǎng)

京東購物小程序cookie方案實(shí)踐

發(fā)布時(shí)間:2020-05-22 10:18 所屬欄目:小程序開發(fā)教程

早期為了解決“會(huì)話保持”的需求,社區(qū)中出現(xiàn)了「cookie方案」并最終成為W3C標(biāo)準(zhǔn):當(dāng)某個(gè)網(wǎng)站登錄成功后,客戶端(瀏覽器)收到一個(gè)cookie標(biāo)識(shí)(文本)并保存下來,在后續(xù)請(qǐng)求中會(huì)自動(dòng)帶上這個(gè)字段,由此Web后臺(tái)可以判斷是否同一個(gè)用戶,從而使“會(huì)話”得以延續(xù)。

微信小程序沒有像瀏覽器一樣內(nèi)置實(shí)現(xiàn)了cookie方案,需要開發(fā)者自行模擬,而原先京東購物小程序及京喜小程序(現(xiàn)微信一級(jí)購物入口)是從微信及手Q購物H5中遷移迭代出來的,也就是說我們不僅要在小程序中模擬一套cookie方案,并且要保持和原業(yè)務(wù)對(duì)cookie處理邏輯的一致,為此我們將實(shí)現(xiàn)方向確定為“基于小程序開放能力,和瀏覽器保持一致”。

微信小程序開放了 數(shù)據(jù)緩存 Storage 和 網(wǎng)絡(luò) Network 這兩種能力,通過這兩套API,我們可以自行DIY一個(gè)cookie方案。

PS:本文所有代碼及使用示例都可以 在這里 找到,閱讀本文時(shí)配合實(shí)踐,效果更佳。

二、瀏覽器中的cookie

為了保持后端對(duì)cookie的處理邏輯和原來的H5一致,小程序的實(shí)現(xiàn)需要往瀏覽器看齊。

所以模擬小程序的cookie前,先看看瀏覽器的cookie機(jī)制,主要有以下幾個(gè)部分:

  • 本地存儲(chǔ):瀏覽器會(huì)在本地分配一塊空間,存儲(chǔ)cookie
  • 請(qǐng)求攜帶:每次發(fā)起請(qǐng)求,都會(huì)從本地取出cookie并追加在請(qǐng)求頭上
  • 響應(yīng)設(shè)置:當(dāng)響應(yīng)頭有Set-Cookie字段時(shí),需要解析并更新
  • 過期時(shí)間:每個(gè)cookie字段有單獨(dú)的過期時(shí)間,并且到期會(huì)自動(dòng)清除
  • 讀寫操作:暴露API給前端JS調(diào)用,可進(jìn)行增刪改查操作
  • 作用域:路徑path、域名domin
  • 編碼:cookie值,在網(wǎng)絡(luò)傳輸需要encode,建議存儲(chǔ)也一樣
  • 其它:HttpOnly、Secure、SameSite

在瀏覽器的 DevTools 中,可以看到當(dāng)前站點(diǎn)下的Cookie明細(xì):

三、小程序中的cookie實(shí)現(xiàn)

方案設(shè)計(jì)

在小程序中模擬Cookie,主要涉及五個(gè)部分:

其中我們會(huì)重點(diǎn)關(guān)注 「Cookie基礎(chǔ)庫」 的實(shí)現(xiàn),另外也會(huì)給出「Request基礎(chǔ)庫」的封裝示例。

本地存儲(chǔ)

小程序提供了 「數(shù)據(jù)緩存 Storage API」(可以理解為Web規(guī)范中的 LocalStorage ),支持存儲(chǔ)“原生類型、Date、及能夠通過JSON.stringify序列化的對(duì)象”。

我們可以利用這些API,在Storage中新開一個(gè) cookies 字段進(jìn)行存儲(chǔ):

// 存:
wx.setStorageSync('cookies', cookies)
// ?。?/span>
wx.getStorageSync('cookies')
復(fù)制代碼

其中 cookies 的「存儲(chǔ)結(jié)構(gòu)」如下:

// cookies = 
{
    cookie1: { // “最小cookie單元” ==> cookieItem
        name: 'cookie1', // cookie名
        value: 'xxx',    // cookie值
        expires: 'Fri, 17 Jan 2020 08:49:41 GMT' // 過期時(shí)間,使用GMT(格林威治標(biāo)準(zhǔn)時(shí)間)格式
    }
},
復(fù)制代碼

上面的 cookie1 便是一個(gè)“最小cookie單元 cookieItem ”,包含了3個(gè)字段(name、value、expires),是本文中定義的「標(biāo)準(zhǔn)cookie格式」,也是cookie操作的基本單元。

打開【微信開發(fā)工具】的 Storage 選項(xiàng)卡,可以查看本地存儲(chǔ)的情況:

讀寫操作

這部分主要作為“公共基礎(chǔ)庫“的角色,為外部業(yè)務(wù)提供增刪改查cookie的API。

1. 獲取cookie———— getCookie()

步驟:從Storage中取出完整cookies ==> 取出指定name的cookie項(xiàng) ==> 校驗(yàn)有效期 ==> 返回值value

實(shí)現(xiàn)如下:

function getCookie(name = '') {
    let cookies = wx.getStorageSync('cookies') // try/catch 略過
    let { value, expires } = cookies[name] || {}

    return (name && expires && !isExpired(expires)) ? decodeURIComponent(cookieItem.value) : ''
}
復(fù)制代碼

2. 設(shè)置cookie———— setCookie()

步驟:從Storage中取出完整cookies ==> 解析入?yún)?==> 覆蓋更新 ==> 同步到本地Storage

首先看下本API設(shè)計(jì)需求:

  • 設(shè)置單個(gè)/多個(gè)cookie
  • 直接傳值/傳cookieItem(Object)
  • 時(shí)間格式maxAge/expires

調(diào)用示例如下:

setCookie({
    cookie1: 12345,
    cookie2: '12345'
})

setCookie({
    cookie1: {
        value: 12345,
        maxAge: 3600 * 24  // 自定義有效期(這里示例是24小時(shí))
    },
    cookie2: {
        value: '12345',
        expires: 'Wed, 21 Oct 2015 07:28:00 GMT' // 標(biāo)準(zhǔn)GMT格式
    }
})
復(fù)制代碼

這里可對(duì)入?yún)⒈闅v,而cookie子項(xiàng)無論直接傳值value還是傳了詳細(xì)object,都盡量的獲取 name/value/expires/maxAge ,傳給格式化函數(shù)轉(zhuǎn)為標(biāo)準(zhǔn)的 cookieItem :

function setCookie(cookiesParam) {
    let oldCookies = wx.getStorageSync('cookies') // try/catch 略過
    let newCookies = {} // 由 cookiesParam 轉(zhuǎn)化為標(biāo)準(zhǔn)格式后的cookies

    for (let name in cookiesParam) {
        if (isObject(cookiesParam[name])) { // 傳入是Object格式
            let { value, expires, maxAge } = cookiesParam[name]
            // 轉(zhuǎn)換為標(biāo)準(zhǔn)cookie格式(cookieItem)
            newCookies[name] = getStandardCookieItem({ name, value, expires, maxAge })
        } else {
            newCookies[name] = getStandardCookieItem({ name, value: cookiesParam[name] })
        }
    }

    // 同步到本地Storage
    saveCookiesToStorage(Object.assign({}, oldCookies, newCookies))
}
復(fù)制代碼

3. 刪除cookie———— removeCookie()

步驟:從Storage中取出完整cookies ==> 刪除指定的cookie項(xiàng) ==> 同步到本地Storage

function removeCookie(cookieName) {
    let cookies = wx.getStorageSync('cookies') // try/catch 略過

    delete cookies[cookieName]

    saveCookiesToStorage(Object.assign({}, cookies))
}
復(fù)制代碼

四、Cookie 在網(wǎng)絡(luò)中的傳遞

本節(jié)主要簡單實(shí)現(xiàn)設(shè)計(jì)圖中的【Request基礎(chǔ)庫】部分

如上圖所示,Cookie在網(wǎng)絡(luò)中的傳輸主要有四個(gè)過程:

Set-Cookie
Cookie
Cookie

以下是對(duì)一個(gè)請(qǐng)求的抓包示例:

在小程序中,請(qǐng)求發(fā)起有兩種方式: HTTP 和 WebSocket ,這里以HTTP為例,先對(duì)請(qǐng)求api進(jìn)行「封裝」:

function requestPro({ url, data, header, method = 'GET' }) {
    return new Promise((resolve, reject) => {
        wx.request({
            url,
            data,
            header: Object.assign({}, { 'Cookie': CookieLib.getCookiesStr() }, header), // 請(qǐng)求頭————帶上Cookie
            success (res) {
              let { data : resData, header, statusCode } = res
              let setCookieStr = header['Set-Cookie'] || header['set-cookie'] || ''

              CookieLib.setCookieFromHeader(setCookieStr) // 響應(yīng)頭————解析Set-Cookie
              resolve(resData)
            },
            fail (err) {
                reject(err)
            }
          })
    })
}
復(fù)制代碼

如上代碼所示,Cookie在前端側(cè)請(qǐng)求模塊中的處理主要有3點(diǎn):

1. 請(qǐng)求攜帶

步驟:(每次發(fā)請(qǐng)求前)從Storage中取出完整cookies ==> 轉(zhuǎn)化為HTTP規(guī)范的請(qǐng)求頭Cookie格式 ==> 設(shè)置到 Request Header 中

上面代碼中的 getCookiesStr() 直接取cookies拼接即可,返回示例: cookie1=xxx;cookie2=yyy 。

2. 響應(yīng)設(shè)置

步驟:(每次收到響應(yīng)后)解析 Response Header 的 Set-Cookie 字段 ==> 轉(zhuǎn)為標(biāo)準(zhǔn)Cookie格式 ==> setCookie()

這里處理 Set-Cookie 內(nèi)容時(shí),有幾個(gè)點(diǎn)需要留意: - 最基本的格式: Set-Cookie: <cookie-name>=<cookie-value> - 可能同時(shí)包含多個(gè)cookie字段,以,分割(但需要排除時(shí)間值里的,) - 時(shí)間格式:Max-Age/Expires (不區(qū)分大小寫)

具體實(shí)現(xiàn)可在文末Demo中找到。

3. 編碼問題

「Cookie值編碼方式」是容易產(chǎn)生困惑的地方,目前看到的廣泛做法都是使用「URL編碼」。

但筆者翻閱 RFC6265 發(fā)現(xiàn),原始規(guī)范中并沒有對(duì)編碼進(jìn)行指定,比如在第四章 Server Requirements (服務(wù)端)中是這樣描述:

To maximize compatibility with user agents, servers that wish to store arbitrary data in a cookie-value SHOULD encode that data, for example, using Base64 [RFC4648].

“為了最好的兼容效果,服務(wù)端應(yīng)該對(duì)cookie值進(jìn)行編碼,例如使用Base64。”

而在第五章 User Agent Requirements (客戶端,也就是瀏覽器),則是“建議以第四章服務(wù)端的實(shí)現(xiàn)為準(zhǔn)”。

總之規(guī)范并沒有指定使用「URL編碼」,但基于該編碼方案已經(jīng)深入人心,也就順其自然成了“默認(rèn)選擇”。那這里也不做例外,瀏覽器怎么做,咋們小程序也保持一致。

在瀏覽器中,推薦cookie值經(jīng)過 encode 編碼后保存下來,所以直接取到的也是 encode 后的值,所以追加在請(qǐng)求頭 Cookie 字段,就不需要 decode 解碼了,直接拼接即可(但基礎(chǔ)庫API的get操作最終需要進(jìn)行 decode 解碼)。

而對(duì)于響應(yīng)頭 Set-Cookie 的值,我們認(rèn)為后端已經(jīng)做了 encode 編碼,所以前端不需要處理,直接存進(jìn) Storage 即可。

五、性能優(yōu)化(高頻讀寫)

前面實(shí)現(xiàn)中每次讀寫cookie都會(huì)調(diào)用小程序Storage API(而且是同步的),小程序框架會(huì)讀寫到本地Storage。 對(duì)于高頻場景,可以將cookie在內(nèi)存中維護(hù)一份,讀寫都直接走「內(nèi)存層」,有更新才同步到「Storage層」。

1. 初始化

首先需要在內(nèi)存中聲明一個(gè) _COOKIES (命名自行diy),建議在cookie基礎(chǔ)庫中聲明,便于統(tǒng)一維護(hù)。

2. 讀

前面初始化時(shí)已經(jīng)從Storage讀取一次cookies,后續(xù)getCookie就直接讀內(nèi)存的 _COOKIES 即可。

3. 寫

寫操作直接更新內(nèi)存,間接更新Storage。 如果有高頻寫場景,可以考慮做個(gè)任務(wù)隊(duì)列進(jìn)行節(jié)流。

六、單元測試

微信官方在2019年5月推出了「小程序自動(dòng)化 SDK」 miniprogram-automator ,經(jīng)過半年多的迭代,目前已基本穩(wěn)定下來。

在購物小程序場景試用了一下,cookie相關(guān)的用例很快就完成了,簡直是開發(fā)者的福音:真香?。?!

實(shí)際項(xiàng)目中,對(duì)cookie的單元測試可以分為兩類:

  1. 小程序全局范圍的cookie驗(yàn)證(比如初始化小程序后,有沒有種下版本號(hào)、訪問行為等關(guān)鍵cookie)
  2. cookie基礎(chǔ)庫API驗(yàn)證(比如get/set/remove等各個(gè)API是否正常工作)

以驗(yàn)證 setCookie() API為例:

it('API驗(yàn)證:setCookie()', async () => {
    await miniProgram.evaluate(() => {
        wx.CookieLib.setCookie({ // 調(diào)用API
            cookie1: 12345,
        })
    })

    let { cookies } = await miniProgram.callWxMethod('getStorageSync', 'cookies')
    expect(cookies['cookie1'].value).toBe(12345) // 期望成功設(shè)置cookie1為12345
})
復(fù)制代碼

這里為了方便測試用例調(diào)用基礎(chǔ)庫API,在小程序啟動(dòng)前,把Cookie基礎(chǔ)庫(CookieLib)掛到了 wx 對(duì)象上,實(shí)現(xiàn)方式是使用node讀寫文件的API去【植入代碼】:

fs.appendFileSync('./your_project/app.js', ''\n wx.CookieUtil = require(\'./lib/cookie.js\');\n'')
復(fù)制代碼

七、Cookie安全

Cookie安全是一個(gè)比較大的話題,這里只簡單列出和小程序相關(guān)的幾個(gè)點(diǎn)。

path、domin、HttpOnly、Secure、SameSite

小程序中已經(jīng)做了一些安全措施,比如只能走HTTPS、合法域名需要管理員到微信后臺(tái)進(jìn)行配置、Storage只能由寫入它的小程序中訪問,等等。 因此 path、domin、HttpOnly、Secure、SameSite 這些字段在小程序環(huán)境下的價(jià)值沒有瀏覽器環(huán)境大,本例中沒有使用(懶..),而實(shí)際業(yè)務(wù)場景可以按自身情況決定是否要使用。

白名單機(jī)制

  1. 前端維護(hù)(大小/數(shù)量) 通常瀏覽器保持的Cookie數(shù)據(jù)不超過4k,部分瀏覽器限制同一站點(diǎn)最多cookie數(shù)為20個(gè)。 如果業(yè)務(wù)龐大的話,建議在Cookie基礎(chǔ)庫做一套「白名單」機(jī)制,在白名單內(nèi)才可以寫入,以此防止“非法寫入”或“內(nèi)容超大導(dǎo)致信息丟失”的問題。

  2. 后臺(tái)維護(hù)(網(wǎng)關(guān)白名單) 同樣的,建議從網(wǎng)關(guān)層面,建立一個(gè)“可信cookie”白名單,自動(dòng)過濾請(qǐng)求中的“非法cookie”字段。

前端防篡改

小程序前端更多是防“誤改”————即在操作Cookie過程中,發(fā)生了意料之外的修改。通常發(fā)生在JS“引用拷貝”特性上,比如前面提到的內(nèi)存維護(hù)一個(gè) _Cookies ,如果有一個(gè)API getAllCookies() 直接將這份內(nèi)存版cookies暴露出去,對(duì)象引用容易被連帶修改。所以cookie基礎(chǔ)庫需要控制暴露API的能力范圍,并對(duì)取值進(jìn)行“深拷貝”。

Session

Session機(jī)制將用戶狀態(tài)放在了服務(wù)端維護(hù),具備更好的安全性,而且目前各種后端對(duì)于session的存儲(chǔ)和同步都有很成熟的技術(shù)方案,有條件的業(yè)務(wù)應(yīng)以Session為主做會(huì)話保持。

指紋上報(bào)

用戶訪問時(shí)生成設(shè)備指紋并上報(bào)(通常是登錄/結(jié)算等環(huán)節(jié)),業(yè)務(wù)后臺(tái)配合風(fēng)控系統(tǒng),遇到異常請(qǐng)求時(shí)下發(fā)驗(yàn)證環(huán)節(jié)。

八、完整小程序?qū)崿F(xiàn)Demo

代碼片段: developers.weixin.qq.com/s/x4sFASmh7…

九、小結(jié)

本文先解析了瀏覽器的 Cookie機(jī)制 運(yùn)作原理,然后使用「數(shù)據(jù)緩存」和「網(wǎng)絡(luò)」能力,以 公共基礎(chǔ)庫 的形式,在小程序中實(shí)現(xiàn)了一套 Cookie方案。希望對(duì)大家有所幫助。



易優(yōu)小程序(企業(yè)版)+靈活api+前后代碼開源 碼云倉庫:starfork
本文地址:http://22321a.com/wxmini/doc/course/25265.html 復(fù)制鏈接 如需定制請(qǐng)聯(lián)系易優(yōu)客服咨詢:800182392 點(diǎn)擊咨詢
QQ在線咨詢