记一场与 cookie 的相遇
简介: cookie 翻译过来为 “小甜点,一种酥性甜饼干,很美味的...”,咳咳,打住!我们这里说的是 “甜点” 文件,它是浏览器储存在用户电脑上的一小段纯文本格式的文件。
由于 http 是一种无状态的协议(无状态是指对于客户端每次发送的请求都认为它是一个新的请求,上一次会话和下一次会话没有联系),服务器无法知道两个请求是否来自于同一个浏览器,因此 cookie 应运而生。cookie 可以记录用户的有关信息,最根本的是它可以帮助 Web 站点保存有关访问者的信息。
我们通过一张图来说明下 cookie 的原理:
客户端向服务端发送请求;接收到请求后,服务端在响应头中通过 set-cookie 携带 cookie 信息返回给客户端;客户端再次发送携带了 cookie 的请求;服务端根据 request header 里的 cookie 信息校验该请求,给出响应。
接下来我们需要先简要了解下 cookie 的属性 [1][2]。
cookie 属性
1. Name
cookie 的名字,相同域名只允许存在一个同名 cookie;一旦创建,该名称便不可更改。
document.cookie = `jxi-m-sid=${cookie}`;
jxi-m-sid 即为新建的 cookie 名字,该名字创建后不能修改,如果需要新的 cookie 只能再次创建。
2. Value
该 cookie 的值,如果值为 Unicode 字符,需要为字符编码;如果值为二进制数据,则需要使用 BASE64 编码。
document.cookie = `jxi-m-sid=${cookie}`;
${cookie} 即为 cookie jxi-m-sid 的值,当需要修改值时,比如将 jxi-m-sid 值改为 2019,操作如下:
document.cookie = `jxi-m-sid=2019`;
3. Domain
可以访问该 cookie 的域名。默认情况下,domain 会被设置为创建该 cookie 的页面所在的域名,所以当给相同域名发送请求时该 cookie 会被发送至服务器。
有关 domain 的设置需要注意以下几点:
(1)设置 domain 时,前面带点和不带点的区别是:
带点:任何子域名都可以访问,包括父域名;
document.cookie = `jxi-m-sid=${cookie};domain=.xx.com;path=/`;
不带点:只有完全一样的域名才可以访问(IE除外,仍然支持子域名访问)
document.cookie = `jxi-m-sid=${cookie};domain=xx.com;path=/`;
(2)非顶级域名,如二级域名或者三级域名,设置的 cookie 的 domain 只能为顶级域名或者二级域名或者三级域名本身,不能设置其他二级域名的 cookie ,否则 cookie 无法生成;
(3)二级域名能读取设置了 domain 为顶级域名或者自身的 cookie,不能读取其他二级域名 domain 的 cookie 。所以要想 cookie 在多个二级域名中共享,需要设置 domain 为顶级域名;
(4)顶级域名只能获取到 domain 设置为顶级域名的 cookie 。
4. Path
可以访问该 cookie 的页面路径。比如 domain 是 abc.com ,path是 /test ,那么只有 /test 路径下的页面可以读取该 cookie。
document.cookie = `jxi-m-sid=${cookie};domain=.abc.com;path=/test`;
5. Expires/Max-Age
该 cookie 的超时时间。若设置其值为一个时间,当到达此时间后,该 cookie 失效。默认有效期为 session ,即会话 cookie 。当浏览器关闭(不是浏览器标签页,而是整个浏览器) 后,该 cookie 失效。
例如,我们希望该 cookie jxi-m-sid 过期,则可以将它的超时时间设置为客户端本地时间1分钟以前,代码如下:
var exp = new Date(); //获取客户端本地当前系统时间 exp.setTime(exp.getTime() - 60 * 1000); //将 exp 设置为客户端本地时间1分钟以前 document.cookie = `jxi-m-sid=;expires=${exp.toUTCString()};domain=.xx.com;path=/`;
注意:expires 必须是 GMT 格式的时间。
6. Size
该 cookie 的大小。cookie 的大小约 4K 左右,在所有浏览器中,任何 cookie 大小超过限制都会被忽略,且永远不会被设置。
7. HTTP
cookie 的 httponly 属性。默认情况下,httpOnly 选项为空,允许客户端通过 js 去访问(包括读取、修改、删除等)该 cookie ;若此属性为 true ,则只有在 http 请求头中会带有此 cookie 的信息,而不能通过 document.cookie 来访问此 cookie,意在提供一个安全措施来帮助阻止通过 JavaScript 发起的跨站脚本攻击 (XSS) 窃取 cookie 的行为。
比如,服务端将 uid 设置为不允许客户端修改:
response.addHeader("Set-Cookie", "uid=112; Path=/; HttpOnly");
8. Secure
用来设置 cookie 只在确保安全的请求中才会发送。当请求是 https 或者其他安全协议时,包含 secure 选项的 cookie 才能被发送至服务器。默认情况下,secure 选项为空,不管是 https 协议还是 http 协议的请求,cookie 都会被发送至服务端。若想在客户端通过 js 去设置 secure 类型的 cookie,必须保证网页是 https 协议的。比如:
document.cookie = `jxi-m-sid=${cookie};secure`;
cookie 的属性介绍完了,关于其使用需要注意:
(1)每个 web 服务器(域名)保存的 cookie 数不能超过 50 个,每个 cookie 大小不能超过 4KB;
(2)尽量让 cookie 的权限范围小一些,能子域可见 domain 绝不设为主域;
(3)存储非敏感的用户信息且设置合理的过期时间,减少因此带来的网络流量(文档传输的负载)。
cookie 的分类
cookie 可以分为两类 [3]:会话 cookie 和持久 cookie。
会话 cookie 是一种临时 cookie,若没有设置有效期,当用户关闭浏览器时,该 cookie 就会被删除。
对于设置了有效期的 cookie 就被称为持久 cookie,它可以存储在硬盘上,当用户关闭浏览器或者机器重启时,该 cookie 依然存在,可以再次被读取使用。
在常用的数据存储方法中,除了本文介绍的 cookie,还有 localStorage、sessionStorage、 session。下面简要介绍下这几种方法的区别:
cookie 与 session
session 机制是一种服务器端的机制,服务器使用一种类似于散列表的结构(也可能就是使用散列表)来保存信息。大多数的应用都是用 cookie 来实现 session 的跟踪,第一次创建 session 的时候,服务端会在 http 协议中告诉客户端,需要在 cookie 里面记录一个 sessionid ,以后每次请求把这个会话 id 发送到服务器。
cookie 与 session 的区别 [4] 如下表所示:
特性 | cookie | session |
存取方式 | 只能存储 ASCII 字符串 | 存取任何类型的数据 |
隐私策略 | 存储在客户端,对用户可见且用户可对其处理 | 存储在服务器 |
有效期 | 可以通过设置较大的过期时间实现长期有效 | 若设置的超时时间过长,容易导致内存溢出 |
服务器压力 | 不占用服务器资源 | 并发用户过多时会耗费大量内存 |
浏览器支持 | 需要浏览器支持,不支持时可以通过 session 和 URL 地址重写实现 | 只在当前窗口和其子窗口内有效 |
跨域 | 支持 | 不支持 |
关于 cookie 与 localStorage、sessionStorage 的区别也整理了一份图表如下所示:
特性 | cookie | localStorage | sessionStorage |
生命周期 | 一般由服务器生成,可设置失效时间。如果是浏览器端生成,默认关闭浏览器后失效 | 除非被清除,否则永久保存 | 仅在当前会话下有效,关闭页面或浏览器后被清除 |
存放数据大小 | 4K 左右 | 一般为 5MB | 同 localStorage |
与服务器端通信 | 始终携带在 http 头中,使用过多会带来性能问题 | 仅在客户端中保存,不和服务器的通信 | 同 localStorage |
易用性 | 需要程序员自己封装,源生的 cookie 接口不友好 | 源生接口可以接受,也可再次封装 | 同 localStorage |
需要注意的是,向 cookie、localStorage 和 sessionStorage 中存储数据时,都需要时刻注意是否有代码存在 XSS 注入的风险,避免存入一些敏感数据。
重点来了:结合项目实践中的问题讨论下 cookie 跨域存储的问题。
cookie 实践
项目背景:在 a 小程序中通过 web-view 内嵌了 b 项目(H5,域名为 b.mm.com)和 c 项目(H5,域名为 c.mm.com),b 和 c 都调用了公共地址接口 interface(interface 所在的域名为 c.mm.com)。在 a 中登录成功之后,需要获取接口返回的 cookie 封装在每个接口的 request header 中,同时需要传递给 b、c;b 和 c 需要获取传递的 cookie 之后,写入到 interface 可访问的域名下,实现用户登录状态的验证。
遇到的问题:小程序内访问线上项目 c 可以正常使用,访问 b 时会偶尔出现登录验证失败的情况。
服务端反馈传入的 cookie 值有误,导致登录验证不通过。浏览器中查看 cookie 发现:存在两个同名但 domain 不同的 cookie。
问题定位:项目 b、c 获取 cookie 的方法是一样的,区别就在于 cookie 值的存储与传递。
项目 b 中 cookie 处理流程:
(1)服务端:b 系统的 domain 为 mm.com;
//application.yml 文件 ... b: m: domain: mm.com scheme: http ...
(2)前端:获取当前传入的 cookie,写入 mm.com 域名下。
在项目 b 中,b.mm.com 域名下是无法向 c.mm.com 域名下写入 cookie,若要实现跨域写入,只能将 cookie 写入到其父级域名:mm.com。
document.cookie = `jxi-m-sid=${cookie};domain=.mm.com;path=/`;
注意:b 系统中的 domain 设置为 mm.com,主要是为了请求 c.mm.com 下的地址接口时 request header 中携带存入的 cookie 。
项目 c 中 cookie 处理流程:
(1)服务端:c 系统的 domain 为 c.mm.com;
//application.yml 文件 ... c: m: domain: c.mm.com scheme: http ...
(2)前端:获取当前传入的 cookie,写入的域名是 c.mm.com。
document.cookie = `jxi-m-sid=${cookie};domain=c.mm.com;path=/`;
由系统 b 和 c 中 cookie 处理流程可以发现:写入 cookie 时设置的 domain 不同,导致出现了两个 jxi-m-sid 的 cookie,当 b 中请求地址接口时,request header 中携带的是错误的 cookie。
解决方法:为了解决同时出现两个同 key 的 cookie 问题,将 c 中的 domain 统一修改为二级域名:mm.com。与此同时需要注意:服务器端需要将 c 系统的 domain 也改为 mm.com。
...
c:
m:
domain: mm.com
scheme: http
...
document.cookie = `jxi-m-sid=${cookie};domain=.mm.com;path=/`;
这样处理之后,两个同 key cookie 的问题解决了,为了保证写入的 cookie 是唯一的,在每次写入 cookie 之前做了清除同 key cookie 处理(利用 cookie 的超时时间属性)。
removeCookie: (cookieName) => { var cookies = document.cookie.split(";"); for (var i = 0; i < cookies.length; i++) { if (cookies[i].indexOf(" ") == 0) { cookies[i] = cookies[i].substring(1); } if (cookies[i].indexOf(cookieName) == 0) { var exp = new Date(); exp.setTime(exp.getTime() - 60 * 1000); document.cookie = cookies[i] + "=;expires=" + exp.toUTCString() + ";domain=.xx.com;path=/" break; } } } removeCookie('jxi-m-sid');
按照这种方法处理之后,很长一段时间内 cookie 的问题正常了,但是极少情况下还是会偶现服务端取到的 cookie 值不是最新的问题。
终极方案:我们想出了另外一种方案:在 b 和 c 系统所有请求的 request header 中携带 cookie,服务端校验用户身份时,首先会从 request header 中获取,没有的话再从写入的 cookie 中取值,解决了上述前后端 cookie 值不一致问题。
接下来我们将项目开发中整个 cookie 处理过程分为以下四步,简要说下每一步的处理过程:
第一步:小程序中获取及传递 cookie;
登录成功时,后端会将 cookie 种在 response header 的 set-cookie 中,我们获取到该 cookie 后先进行本地存储,然后封装到每次接口的 request header 中。
//获取 cookie wx.setStorageSync('cookie', res.header["Set-Cookie"]); //统一封装 request 请求 const request = parameter => { //url必填项 if (!parameter || parameter == {} || !parameter.url) { console.log('Data request can not be executed without URL.'); return false; } else { var murl = parameter.url; var headerCookie = wx.getStorageSync('cookie'); //判断是否有独自cookie请求 var selfCookie = parameter.selfCookie; selfCookie && (headerCookie += selfCookie); wx.request({ url: murl, data: parameter.data || {}, header: { 'Cookie': headerCookie }, method: parameter.method || 'POST', success: function(res) { parameter.success && parameter.success(res); }, fail: function(e) { parameter.fail && parameter.fail(e); }, complete: function() { parameter.complete && parameter.complete(); } }); } }
这样在小程序内,每个接口的 request header 中都会携带该 cookie,服务端可以根据该 cookie 判断用户的登录状态。
第二步:小程序向 web-view 内嵌的 H5 中传递 cookie;
第三步:H5 中获取小程序传过来的 cookie ,通过 js 写入 [5];
获取 cookie:
getQueryString: (name) => { let reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)"); let r = window.location.search.substr(1).match(reg); if (r != null) { return unescape(decodeURI(r[2])); return null; } } let cookie = getQueryString('cookie');
通过 js 将 cookie 写入到父级域名:mm.com。写入方法如下:
document.cookie = `jxi-m-sid=${cookie};domain=.mm.com;path=/`;
第四步:接口 request header 中统一封装 key 为 jxi-m-sid 的 cookie,以 axios 库的 get 和 post 请求为例( headers 里定义):
export default { post(url, data) { return axios({ method: 'post', url, data: JSON.stringify(data), timeout: 30000, headers: { 'jxi-m-sid': cookie } }) }, get(url, params) { return axios({ method: 'get', url, params, timeout: 30000, headers: { 'jxi-m-sid': cookie } }) } }
以上就是基于我们的项目背景,解决两个同 key cookie 且前后端获取值不一致问题的解决方案。涉及到 cookie 的 domain、path 问题请大家使用时高度重视,这将决定你所写入 cookie 的唯一性。
cookie 一般是用来存储当前登录用户的会话信息且是存储在客户端,用户可以随意修改,所以存在一定的风险。针对这个问题,也有了比较成熟的解决方法,这里我们简要介绍下。
cookie 防篡改机制
敏感数据存储在服务器
敏感数据避免存储在 cookie 里,可以根据 sessionid 将其存储在服务端。需要时根据 sessionid 获取即可。
防篡改签名
服务端为每个 cookie 生成签名,如果用户篡改了该 cookie 则签名是不一致的,服务端可以以此来判断该 cookie 是否被篡改。
具体的实现步骤可以如下所示:
(1)服务端提供签名生成算法 secret;
(2)根据方法生成签名 secret(x);
(3)将生成的签名放到 cookie 中,可以使用 | 将 cookie 内容与签名分隔开,如 name=x|yyyyy;
(4)服务端校验收到的内容和签名,判断是否被篡改。
以上的方法可以进一步确保 cookie 的数据安全 [6],在有需要的项目中大家可以尝试使用下。
小结
本文主要介绍了 cookie 的相关知识,包括:常用属性、修改、跨域存储、防篡改等。对于 cookie 的跨域传递,上述项目实践中的方法是我们的使用方法,大家在使用的时候可以根据自己项目的情况进行选择。
敲黑板:cookie 中不要放敏感信息哦,再次友情提示~好了,关于 cookie 的问题就介绍到这里了,如有任何疑问,欢迎留言。
扩展阅读
[1] https://www.quirksmode.org/js/cookies.html
[2] http://bubkoo.com/2014/04/21/http-cookies-explained/
[3] http://www.allaboutcookies.org/cookies/cookies-the-same.html
[4] https://www.jianshu.com/p/25802021be63
[5] http://www.tutorialspoint.com/javascript/javascript_cookies.htm