京东购物小程序cookie方案实践

做者:张志豪html

1、前言

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

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

微信小程序开放了 数据缓存 Storage网络 Network 这两种能力,经过这两套API,咱们能够自行DIY一个cookie方案。小程序

PS:本文全部代码及使用示例均可以 在这里 找到,阅读本文时配合实践,效果更佳。后端

2、浏览器中的cookie

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

因此模拟小程序的cookie前,先看看浏览器的cookie机制,主要有如下几个部分:api

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

在浏览器的DevTools中,能够看到当前站点下的Cookie明细: 浏览器

Chrome Cookie面板截图

3、小程序中的cookie实现

方案设计

在小程序中模拟Cookie,主要涉及五个部分: 缓存

小程序cookie方案设计图
其中咱们会重点关注 「Cookie基础库」 的实现,另外也会给出「Request基础库」的封装示例。

本地存储

小程序提供了 「数据缓存 Storage API」(能够理解为Web规范中的LocalStorage),支持存储“原生类型、Date、及可以经过JSON.stringify序列化的对象”。安全

咱们能够利用这些API,在Storage中新开一个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(格林威治标准时间)格式
    }
},
复制代码

上面的cookie1即是一个“最小cookie单元cookieItem”,包含了3个字段(name、value、expires),是本文中定义的「标准cookie格式」,也是cookie操做的基本单元。

打开【微信开发工具】的Storage选项卡,能够查看本地存储的状况:

Storage面板截图

读写操做

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

1. 获取cookie————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) : ''
}
复制代码

2. 设置cookie————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格式
    }
})
复制代码

这里可对入参遍历,而cookie子项不管直接传值value仍是传了详细object,都尽可能的获取name/value/expires/maxAge,传给格式化函数转为标准的cookieItem

function setCookie(cookiesParam) {
    let oldCookies = wx.getStorageSync('cookies') // try/catch 略过
    let newCookies = {} // 由 cookiesParam 转化为标准格式后的cookies

    for (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))
}
复制代码

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

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

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

    delete cookies[cookieName]

    saveCookiesToStorage(Object.assign({}, cookies))
}
复制代码

4、Cookie 在网络中的传递

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

network-transfer

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

  1. 客户端发起HTTP请求
  2. 服务端响应,并在响应头加上 Set-Cookie,客户端接受并解析保存
  3. 下一次客户端发起HTTP请求,在请求头加上Cookie
  4. 服务端识别出请求头的Cookie,做出相应处理

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

request-demo

在小程序中,请求发起有两种方式:HTTPWebSocket,这里以HTTP为例,先对请求api进行「封装」:

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) // 响应头————解析Set-Cookie
              resolve(resData)
            },
            fail (err) {
                reject(err)
            }
          })
    })
}
复制代码

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

1. 请求携带

步骤:(每次发请求前)从Storage中取出完整cookies ==> 转化为HTTP规范的请求头Cookie格式 ==> 设置到Request Header

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

2. 响应设置

步骤:(每次收到响应后)解析Response HeaderSet-Cookie字段 ==> 转为标准Cookie格式 ==> setCookie()

这里处理Set-Cookie内容时,有几个点须要留意: - 最基本的格式:Set-Cookie: <cookie-name>=<cookie-value> - 可能同时包含多个cookie字段,以,分割(但须要排除时间值里的,) - 时间格式:Max-Age/Expires (不区分大小写)

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

3. 编码问题

「Cookie值编码方式」是容易产生困惑的地方,目前看到的普遍作法都是使用「URL编码」。

但笔者翻阅 RFC6265 发现,原始规范中并无对编码进行指定,好比在第四章 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编码」,但基于该编码方案已经深刻人心,也就顺其天然成了“默认选择”。那这里也不作例外,浏览器怎么作,咋们小程序也保持一致。

在浏览器中,推荐cookie值通过encode编码后保存下来,因此直接取到的也是encode后的值,因此追加在请求头Cookie字段,就不须要decode解码了,直接拼接便可(但基础库API的get操做最终须要进行decode解码)。

而对于响应头Set-Cookie的值,咱们认为后端已经作了encode编码,因此前端不须要处理,直接存进 Storage 便可。

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

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

1. 初始化

首先须要在内存中声明一个_COOKIES(命名自行diy),建议在cookie基础库中声明,便于统一维护。

2. 读

前面初始化时已经从Storage读取一次cookies,后续getCookie就直接读内存的_COOKIES便可。

3. 写

写操做直接更新内存,间接更新Storage。 若是有高频写场景,能够考虑作个任务队列进行节流。

6、单元测试

微信官方在2019年5月推出了「小程序自动化 SDK」 miniprogram-automator,通过半年多的迭代,目前已基本稳定下来。

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

smells-good

实际项目中,对cookie的单元测试能够分为两类:

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

以验证setCookie()API为例:

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
})
复制代码

这里为了方便测试用例调用基础库API,在小程序启动前,把Cookie基础库(CookieLib)挂到了wx对象上,实现方式是使用node读写文件的API去【植入代码】:

fs.appendFileSync('./your_project/app.js', ''\n wx.CookieUtil = require(\'./lib/cookie.js\');\n'')
复制代码

7、Cookie安全

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

path、domin、HttpOnly、Secure、SameSite

小程序中已经作了一些安全措施,好比只能走HTTPS、合法域名须要管理员到微信后台进行配置、Storage只能由写入它的小程序中访问,等等。 所以path、domin、HttpOnly、Secure、SameSite这些字段在小程序环境下的价值没有浏览器环境大,本例中没有使用(懒..),而实际业务场景能够按自身状况决定是否要使用。

白名单机制

  1. 前端维护(大小/数量) 一般浏览器保持的Cookie数据不超过4k,部分浏览器限制同一站点最多cookie数为20个。 若是业务庞大的话,建议在Cookie基础库作一套「白名单」机制,在白名单内才能够写入,以此防止“非法写入”或“内容超大致使信息丢失”的问题。

  2. 后台维护(网关白名单) 一样的,建议从网关层面,创建一个“可信cookie”白名单,自动过滤请求中的“非法cookie”字段。

前端防篡改

小程序前端更可能是防“误改”————即在操做Cookie过程当中,发生了意料以外的修改。一般发生在JS“引用拷贝”特性上,好比前面提到的内存维护一个_Cookies,若是有一个APIgetAllCookies()直接将这分内存版cookies暴露出去,对象引用容易被连带修改。因此cookie基础库须要控制暴露API的能力范围,并对取值进行“深拷贝”。

Session

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

指纹上报

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

8、完整小程序实现Demo

代码片断:developers.weixin.qq.com/s/x4sFASmh7…

Demo工程截图

9、小结

本文先解析了浏览器的 Cookie机制 运做原理,而后使用「数据缓存」和「网络」能力,以 公共基础库 的形式,在小程序中实现了一套 Cookie方案。但愿对你们有所帮助。

10、相关连接


若是你以为这篇内容对你有价值,请点赞,并关注咱们的官网和咱们的微信公众号(WecTeam),每周都有优质文章推送:

WecTeam
相关文章
相关标签/搜索