记一场与 cookie 的相遇

简介: cookie 翻译过来为 “小甜点,一种酥性甜饼干,很美味的...”,咳咳,打住!咱们这里说的是 “甜点” 文件,它是浏览器储存在用户电脑上的一小段纯文本格式的文件。

因为 http 是一种无状态的协议(无状态是指对于客户端每次发送的请求都认为它是一个新的请求,上一次会话和下一次会话没有联系),服务器没法知道两个请求是否来自于同一个浏览器,所以 cookie 应运而生。cookie 能够记录用户的有关信息,最根本的是它能够帮助 Web 站点保存有关访问者的信息。javascript

咱们经过一张图来讲明下 cookie 的原理:html

 

客户端向服务端发送请求;接收到请求后,服务端在响应头中经过 set-cookie 携带 cookie 信息返回给客户端;客户端再次发送携带了 cookie 的请求;服务端根据 request header 里的 cookie 信息校验该请求,给出响应。前端

接下来咱们须要先简要了解下 cookie 的属性 [1][2]。java

cookie 属性

1. Nameios

cookie 的名字,相同域名只容许存在一个同名 cookie;一旦建立,该名称便不可更改。程序员

document.cookie = `jxi-m-sid=${cookie}`;

jxi-m-sid 即为新建的 cookie 名字,该名字建立后不能修改,若是须要新的 cookie 只能再次建立。web

2. Value算法

该 cookie 的值,若是值为 Unicode 字符,须要为字符编码;若是值为二进制数据,则须要使用 BASE64 编码。axios

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

[6] http://www.javashuo.com/article/p-eqtpbghv-dt.html

相关文章
相关标签/搜索