一、前言



早期为了解决“会话保持”的需求,社区中出现了「COOKIE 方案」并最终成为 W3C 标准:当某个网站登录成功后,客户端(浏览器)收到一个 COOKIE 标识(文本)并保存下来,在后续请求中会自动带上这个字段,由此 Web 后台可以判断是否同一个用户,从而使“会话”得以延续。



微信小程序没有像浏览器一样内置实现了 COOKIE 方案,需要开发者自行模拟,而原先京东购物小程序及京喜小程序(现微信一级购物入口)是从微信及手 Q 购物入口 H5 中迁移迭代出来的,也就是说我们不仅要在小程序中模拟一套 COOKIE 方案,并且要保持和原业务对 COOKIE 处理逻辑的一致,为此我们将实现方向确定为“基于小程序开放能力,和浏览器保持一致”。


微信小程序开放了 数据缓存 Storage[1] 和 网络 Network[2] 这两种能力,通过这两套 API,我们可以自行 DIY 一个 COOKIE 方案。


PS:本文所有代码及使用示例都可以 在这里[3] 找到,阅读本文时配合实践,效果更佳。


二、浏览器中的 COOKIE


为了保持后端对 COOKIE 的处理逻辑和原来的 H5 一致,小程序的实现需要往浏览器看齐。


所以模拟小程序的 COOKIE 前,先看看浏览器的 COOKIE 机制,主要有以下几个部分:


  • 本地存储:浏览器会在本地分配一块空间,存储 COOKIE
  • 请求携带:每次发起请求,都会从本地取出 COOKIE 并追加在请求头上
  • 响应设置:当响应头有 Set-COOKIE 字段时,需要解析并更新
  • 读写操作:暴露 API 给前端 JS 调用,可进行增删改查操作
  • 作用域:路径 path、域名 domin
  • 编码:COOKIE 值,在网络传输需要 encode,建议存储也一样
  • 其它:HttpOnly、Secure、SameSite
DevTools

三、小程序中的 COOKIE 实现


方案设计


在小程序中模拟 COOKIE,主要涉及五个部分:其中我们会重点关注 「COOKIE 基础库」 的实现,另外也会给出「Request 基础库」的封装示例。


本地存储


LocalStorage

COOKIEs

// 存:wx.setStorageSync('COOKIEs', COOKIEs)// 取:wx.getStorageSync('COOKIEs')
COOKIEs

// COOKIEs ={COOKIE1: { // “最小 COOKIE 单元” ==> COOKIEItem        name: 'COOKIE1', // COOKIE 名        value: 'xxx',    // COOKIE 值        expires: 'Fri, 17 Jan 2020 08:49:41 GMT' // 过期时间,使用 GMT(格林威治标准时间)格式    }},
COOKIE1COOKIEItem

Storage

读写操作



这部分主要作为“公共基础库“的角色,为外部业务提供增删改查 COOKIE 的 API。



getCOOKIE()

步骤:从 Storage 中取出完整 COOKIEs ==> 取出指定 name 的 COOKIE 项 ==> 校验有效期 ==> 返回值 value


实现如下:


function getCOOKIE(name = '') {let COOKIEs = wx.getStorageSync('COOKIEs') // try/catch 略过let { value, expires } = COOKIEs[name] || {}return (name && expires && !isExpired(expires)) ? decodeURIComponent(COOKIEItem.value) : ''}
setCOOKIE()

步骤:从 Storage 中取出完整 COOKIEs ==> 解析入参 ==> 覆盖更新 ==> 同步到本地 Storage


首先看下本 API 设计需求:


  • 设置单个/多个 COOKIE
  • 直接传值/传 COOKIEItem(Object)
  • 时间格式 maxAge/expires

调用示例如下:


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' // 标准 GMT 格式    }})
name/value/expires/maxAgeCOOKIEItem

function setCOOKIE(COOKIEsParam) {let oldCOOKIEs = wx.getStorageSync('COOKIEs') // try/catch 略过let newCOOKIEs = {} // 由 COOKIEsParam 转化为标准格式后的 COOKIEsfor (let name in COOKIEsParam) {if (isObject(COOKIEsParam[name])) { // 传入是 Object 格式let { value, expires, maxAge } = COOKIEsParam[name]// 转换为标准 COOKIE 格式(COOKIEItem)            newCOOKIEs[name] = getStandardCOOKIEItem({ name, value, expires, maxAge })        } else {            newCOOKIEs[name] = getStandardCOOKIEItem({ name, value: COOKIEsParam[name] })        }    }// 同步到本地 Storage    saveCOOKIEsToStorage(Object.assign({}, oldCOOKIEs, newCOOKIEs))}
removeCOOKIE()

步骤:从 Storage 中取出完整 COOKIEs ==> 删除指定的 COOKIE 项 ==> 同步到本地 Storage


function removeCOOKIE(COOKIEName) {let COOKIEs = wx.getStorageSync('COOKIEs') // try/catch 略过delete COOKIEs[COOKIEName]    saveCOOKIEsToStorage(Object.assign({}, COOKIEs))}

四、COOKIE 在网络中的传递



本节主要简单实现设计图中的【Request 基础库】部分



如上图所示,COOKIE 在网络中的传输主要有四个过程:


Set-COOKIECOOKIECOOKIE

以下是对一个请求的抓包示例:


HTTPWebSocket

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 } = reslet setCOOKIEStr = header['Set-COOKIE'] || header['set-COOKIE'] || ''              COOKIELib.setCOOKIEFromHeader(setCOOKIEStr) // 响应头————解析 Set-COOKIE              resolve(resData)            },            fail (err) {                reject(err)            }          })    })}

如上代码所示,COOKIE 在前端侧请求模块中的处理主要有 3 点:


1. 请求携带


Request Header

getCOOKIEsStr()COOKIE1=xxx;COOKIE2=yyy

2. 响应设置


Response HeaderSet-COOKIE

Set-COOKIESet-COOKIE: =

具体实现可在文末 Demo 中找到。


3. 编码问题


「COOKIE 值编码方式」是容易产生困惑的地方,目前看到的广泛做法都是使用「URL 编码」。


但笔者翻阅 RFC6265[4] 发现,原始规范中并没有对编码进行指定,比如在第四章 Server Requirements (服务端)中是这样描述:



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].



“为了最好的兼容效果,服务端应该对 COOKIE 值进行编码,例如使用 Base64。”


而在第五章 User Agent Requirements (客户端,也就是浏览器),则是“建议以第四章服务端的实现为准”。


总之规范并没有指定使用「URL 编码」,但基于该编码方案已经深入人心,也就顺其自然成了“默认选择”。那这里也不做例外,浏览器怎么做,咱们小程序也保持一致。


encodeencodeCOOKIEdecodedecode

Set-COOKIEencode

五、性能优化(高频读写)


前面实现中每次读写 COOKIE 都会调用小程序 Storage API(而且是同步的),小程序框架会读写到本地 Storage。对于高频场景,可以将 COOKIE 在内存中维护一份,读写都直接走「内存层」,有更新才同步到「Storage 层」。


1. 初始化


_COOKIES

2. 读


_COOKIES

3. 写


写操作直接更新内存,间接更新 Storage。如果有高频写场景,可以考虑做个任务队列进行节流。


六、单元测试


miniprogram-automator

在购物小程序场景试用了一下,COOKIE 相关的用例很快就完成了,简直是开发者的福音:真香!!!


实际项目中,对 COOKIE 的单元测试可以分为两类:


  • 小程序全局范围的 COOKIE 验证(比如初始化小程序后,有没有种下版本号、访问行为等关键 COOKIE)
  • COOKIE 基础库 API 验证(比如 get/set/remove 等各个 API 是否正常工作)
setCOOKIE()

it('API 验证:setCOOKIE()', async () => {await miniProgram.evaluate(() => {        wx.COOKIELib.setCOOKIE({ // 调用 API            COOKIE1: 12345,        })    })let { COOKIEs } = await miniProgram.callWxMethod('getStorageSync', 'COOKIEs')    expect(COOKIEs['COOKIE1'].value).toBe(12345) // 期望成功设置 COOKIE1为12345})
wx

fs.appendFileSync('./your_project/app.js', ''n wx.COOKIEUtil = require('./lib/COOKIE.js');n'')

七、COOKIE 安全


COOKIE 安全是一个比较大的话题,这里只简单列出和小程序相关的几个点。


path、domin、HttpOnly、Secure、SameSite


path、domin、HttpOnly、Secure、SameSite

白名单机制


1 前端维护(大小/数量)


通常浏览器保持的 COOKIE 数据不超过 4k,部分浏览器限制同一站点最多 COOKIE 数为 20 个。


如果业务庞大的话,建议在 COOKIE 基础库做一套「白名单」机制,在白名单内才可以写入,以此防止“非法写入”或“内容超大导致信息丢失”的问题。


2 后台维护(网关白名单)


同样的,建议从网关层面,建立一个“可信 COOKIE”白名单,自动过滤请求中的“非法 COOKIE”字段。


前端防篡改


_COOKIEsgetAllCOOKIEs()

Session


Session 机制将用户状态放在了服务端维护,具备更好的安全性,而且目前各种后端对于 session 的存储和同步都有很成熟的技术方案,有条件的业务应以 Session 为主做会话保持。


指纹上报


用户访问时生成设备指纹并上报(通常是登录/结算等环节),业务后台配合风控系统,遇到异常请求时下发验证环节。


八、完整小程序实现 Demo


代码片段:https://developers.weixin.qq.com/s/x4sFASmh7xdq


九、小结


本文先解析了浏览器的 COOKIE 机制 运作原理,然后使用「数据缓存」和「网络」能力,以 公共基础库 的形式,在小程序中实现了一套 COOKIE 方案。希望对大家有所帮助。


十、相关链接


  • RFC6265(HTTP 状态管理机制规范)[5]
  • HTTP COOKIEs Explained[6]
  • 小程序数据缓存 Storage API[7]
  • 小程序网络 Network API[8]
  • 小程序自动化 SDK[9]
  • 小程序实现 Demo[10]

参考资料


[1]

数据缓存 Storage: https://developers.weixin.qq.com/miniprogram/dev/api/storage/wx.setStorageSync.html


[2]

网络 Network: https://developers.weixin.qq.com/miniprogram/dev/api/network/request/wx.request.html


[3]

在这里: https://developers.weixin.qq.com/s/x4sFASmh7xdq


[4]

RFC6265: https://tools.ietf.org/html/rfc6265


[5]

RFC6265(HTTP 状态管理机制规范): https://tools.ietf.org/html/rfc6265


[6]

HTTP COOKIEs Explained: https://humanwhocodes.com/blog/2009/05/05/http-COOKIEs-explained/


[7]

小程序数据缓存 Storage API: https://developers.weixin.qq.com/miniprogram/dev/api/storage/wx.setStorageSync.html


[8]

小程序网络 Network API: https://developers.weixin.qq.com/miniprogram/dev/api/network/request/wx.request.html


[9]

小程序自动化 SDK: https://developers.weixin.qq.com/miniprogram/dev/devtools/auto/


[10]

小程序实现 Demo: https://developers.weixin.qq.com/s/x4sFASmh7xdq