早期為了解決“會話保持”的需求,社區(qū)中出現(xiàn)了「cookie方案」并最終成為W3C標(biāo)準(zhǔn):當(dāng)某個網(wǎng)站登錄成功后,客戶端(瀏覽器)收到一個cookie標(biāo)識(文本)并保存下來,在后續(xù)請求中會自動帶上這個字段,由此Web后臺可以判斷是否同一個用戶,從而使“會話”得以延續(xù)。 微信小程序沒有像瀏覽器一樣內(nèi)置實現(xiàn)了cookie方案,需要開發(fā)者自行模擬,而原先京東購物小程序及京喜小程序(現(xiàn)微信一級購物入口)是從微信及手Q購物H5中遷移迭代出來的,也就是說我們不僅要在小程序中模擬一套cookie方案,并且要保持和原業(yè)務(wù)對cookie處理邏輯的一致,為此我們將實現(xiàn)方向確定為“基于小程序開放能力,和瀏覽器保持一致”。 微信小程序開放了 數(shù)據(jù)緩存 Storage 和 網(wǎng)絡(luò) Network 這兩種能力,通過這兩套API,我們可以自行DIY一個cookie方案。 PS:本文所有代碼及使用示例都可以 在這里 找到,閱讀本文時配合實踐,效果更佳。 二、瀏覽器中的cookie為了保持后端對cookie的處理邏輯和原來的H5一致,小程序的實現(xiàn)需要往瀏覽器看齊。 所以模擬小程序的cookie前,先看看瀏覽器的cookie機制,主要有以下幾個部分:
在瀏覽器的 三、小程序中的cookie實現(xiàn)方案設(shè)計在小程序中模擬Cookie,主要涉及五個部分: 其中我們會重點關(guān)注 「Cookie基礎(chǔ)庫」 的實現(xiàn),另外也會給出「Request基礎(chǔ)庫」的封裝示例。 本地存儲
小程序提供了 「數(shù)據(jù)緩存 Storage API」(可以理解為Web規(guī)范中的
我們可以利用這些API,在Storage中新開一個
// 存:
wx.setStorageSync('cookies', cookies)
// 取:
wx.getStorageSync('cookies')
復(fù)制代碼
其中
// cookies =
{
cookie1: { // “最小cookie單元” ==> cookieItem
name: 'cookie1', // cookie名
value: 'xxx', // cookie值
expires: 'Fri, 17 Jan 2020 08:49:41 GMT' // 過期時間,使用GMT(格林威治標(biāo)準(zhǔn)時間)格式
}
},
復(fù)制代碼
上面的
打開【微信開發(fā)工具】的 讀寫操作這部分主要作為“公共基礎(chǔ)庫“的角色,為外部業(yè)務(wù)提供增刪改查cookie的API。
1. 獲取cookie———— 步驟:從Storage中取出完整cookies ==> 取出指定name的cookie項 ==> 校驗有效期 ==> 返回值value 實現(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———— 步驟:從Storage中取出完整cookies ==> 解析入?yún)?==> 覆蓋更新 ==> 同步到本地Storage 首先看下本API設(shè)計需求:
調(diào)用示例如下:
setCookie({
cookie1: 12345,
cookie2: '12345'
})
setCookie({
cookie1: {
value: 12345,
maxAge: 3600 * 24 // 自定義有效期(這里示例是24小時)
},
cookie2: {
value: '12345',
expires: 'Wed, 21 Oct 2015 07:28:00 GMT' // 標(biāo)準(zhǔn)GMT格式
}
})
復(fù)制代碼
這里可對入?yún)⒈闅v,而cookie子項無論直接傳值value還是傳了詳細(xì)object,都盡量的獲取
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———— 步驟:從Storage中取出完整cookies ==> 刪除指定的cookie項 ==> 同步到本地Storage
function removeCookie(cookieName) {
let cookies = wx.getStorageSync('cookies') // try/catch 略過
delete cookies[cookieName]
saveCookiesToStorage(Object.assign({}, cookies))
}
復(fù)制代碼
四、Cookie 在網(wǎng)絡(luò)中的傳遞本節(jié)主要簡單實現(xiàn)設(shè)計圖中的【Request基礎(chǔ)庫】部分 如上圖所示,Cookie在網(wǎng)絡(luò)中的傳輸主要有四個過程:
Set-Cookie
Cookie
Cookie
以下是對一個請求的抓包示例:
在小程序中,請求發(fā)起有兩種方式:
function requestPro({ url, data, header, method = 'GET' }) {
return new Promise((resolve, reject) => {
wx.request({
url,
data,
header: Object.assign({}, { 'Cookie': CookieLib.getCookiesStr() }, header), // 請求頭————帶上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è)請求模塊中的處理主要有3點: 1. 請求攜帶
步驟:(每次發(fā)請求前)從Storage中取出完整cookies ==> 轉(zhuǎn)化為HTTP規(guī)范的請求頭Cookie格式 ==> 設(shè)置到
上面代碼中的 2. 響應(yīng)設(shè)置
步驟:(每次收到響應(yīng)后)解析
這里處理 具體實現(xiàn)可在文末Demo中找到。 3. 編碼問題「Cookie值編碼方式」是容易產(chǎn)生困惑的地方,目前看到的廣泛做法都是使用「URL編碼」。 但筆者翻閱 RFC6265 發(fā)現(xiàn),原始規(guī)范中并沒有對編碼進行指定,比如在第四章 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)該對cookie值進行編碼,例如使用Base64。” 而在第五章 User Agent Requirements (客戶端,也就是瀏覽器),則是“建議以第四章服務(wù)端的實現(xiàn)為準(zhǔn)”。 總之規(guī)范并沒有指定使用「URL編碼」,但基于該編碼方案已經(jīng)深入人心,也就順其自然成了“默認(rèn)選擇”。那這里也不做例外,瀏覽器怎么做,咋們小程序也保持一致。
在瀏覽器中,推薦cookie值經(jīng)過
而對于響應(yīng)頭 五、性能優(yōu)化(高頻讀寫)前面實現(xiàn)中每次讀寫cookie都會調(diào)用小程序Storage API(而且是同步的),小程序框架會讀寫到本地Storage。 對于高頻場景,可以將cookie在內(nèi)存中維護一份,讀寫都直接走「內(nèi)存層」,有更新才同步到「Storage層」。 1. 初始化
首先需要在內(nèi)存中聲明一個 2. 讀
前面初始化時已經(jīng)從Storage讀取一次cookies,后續(xù)getCookie就直接讀內(nèi)存的 3. 寫寫操作直接更新內(nèi)存,間接更新Storage。 如果有高頻寫場景,可以考慮做個任務(wù)隊列進行節(jié)流。 六、單元測試
微信官方在2019年5月推出了「小程序自動化 SDK」 在購物小程序場景試用了一下,cookie相關(guān)的用例很快就完成了,簡直是開發(fā)者的福音:真香!!! 實際項目中,對cookie的單元測試可以分為兩類:
以驗證
it('API驗證: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,在小程序啟動前,把Cookie基礎(chǔ)庫(CookieLib)掛到了
fs.appendFileSync('./your_project/app.js', ''\n wx.CookieUtil = require(\'./lib/cookie.js\');\n'')
復(fù)制代碼
七、Cookie安全Cookie安全是一個比較大的話題,這里只簡單列出和小程序相關(guān)的幾個點。 path、domin、HttpOnly、Secure、SameSite
小程序中已經(jīng)做了一些安全措施,比如只能走HTTPS、合法域名需要管理員到微信后臺進行配置、Storage只能由寫入它的小程序中訪問,等等。 因此 白名單機制
前端防篡改
小程序前端更多是防“誤改”————即在操作Cookie過程中,發(fā)生了意料之外的修改。通常發(fā)生在JS“引用拷貝”特性上,比如前面提到的內(nèi)存維護一個 SessionSession機制將用戶狀態(tài)放在了服務(wù)端維護,具備更好的安全性,而且目前各種后端對于session的存儲和同步都有很成熟的技術(shù)方案,有條件的業(yè)務(wù)應(yīng)以Session為主做會話保持。 指紋上報用戶訪問時生成設(shè)備指紋并上報(通常是登錄/結(jié)算等環(huán)節(jié)),業(yè)務(wù)后臺配合風(fēng)控系統(tǒng),遇到異常請求時下發(fā)驗證環(huán)節(jié)。 八、完整小程序?qū)崿F(xiàn)Demo代碼片段: developers.weixin.qq.com/s/x4sFASmh7… 九、小結(jié)本文先解析了瀏覽器的 Cookie機制 運作原理,然后使用「數(shù)據(jù)緩存」和「網(wǎng)絡(luò)」能力,以 公共基礎(chǔ)庫 的形式,在小程序中實現(xiàn)了一套 Cookie方案。希望對大家有所幫助。 |