简介: 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

[6] https://juejin.im/post/5b02fe326fb9a07ab1117c82

记一场与 cookie 的相遇的更多相关文章

  1. 记一次与a标签相遇的小事

    最近做的一个项目,按钮使用的是a标签做的,样子还不错.不过正是这个a标签把我坑死了,有一个场景是点击a标签去调后台服务,为了防止用户频繁点击按钮提交,在去请求后台服务的时候肯定要先把按钮的事件给禁止掉 ...

  2. 想当然是编程最大的坑,记更新删除过期cookie无效有感

    一般来说只要设置了cookie的过期时间,就可以实现删除cookie的作用. 可是我尝试了设置过期时间,清除cookie内容都无效. 最后才发现,我根本没有执行到那段设置过期的代码. 刚开始是因为登出 ...

  3. 记第一场atcoder和codeforces 2018-2019 ICPC, NEERC, Northern Eurasia Finals Online Mirror

    下午连着两场比赛,爽. 首先是codeforses,我和一位dalao一起打的,结果考炸了,幸亏不计rating.. A Alice the Fan 这个就是记忆化搜索一下预处理,然后直接回答询问好了 ...

  4. 记第一场cf比赛(Codeforces915)

    比赛感想 本来21:05开始的比赛,结果记成21:30了...晚了25分钟才开始[捂脸] 这次是Educational Round,所以还比较简单. 前两道题一眼看去模拟+贪心,怕错仔细看了好几遍题, ...

  5. Django边学边记--状态保持(cookie和session)

    Cookie 概念: Cookie,也叫Cookies,指某些网站为了辨别用户身份.进行session跟踪而储存在用户本地终端上的数据(通常经过加密),好比会员卡或餐票. 特点: Cookie是由服务 ...

  6. cookie的存入和取出

    刚刚开始写页面没多久,因为登录注册写的是个tab切换,所以需要在点击登录的时候跳到登录页面,点击注册的时候跳转到注册页面,自己在网上找了一下,研究了一下cookie方法,现在把它记下来. 存入cook ...

  7. 2014.04.28基于CPLD的LCOS场序彩色视频控制器设计

    基于CPLD的LCOS场序彩色视频控制器设计 作者:宋丹娜,代永平,刘艳艳,商广辉 发表刊物:液晶与显示,2009 学习时间:2014.04.28 文章讲述了-- (和上一篇论文有些相似之处) 1. ...

  8. [置顶] PHP开发实战权威指南-读书总结

    从今年开始,断断续续学习PHP已经有4个月了. 最初,认真学习PHP几天,就弄WordPress搭建了一个个人博客,这也符合技术人的实践理念. 最近,重温PHP开发实战权威指南,做点总结,整理下自己学 ...

  9. PHP开发实战权威指南-读书总结

    从今年开始,断断续续学习PHP已经有4个月了.最初,认真学习PHP几天,就弄WordPress搭建了一个个人博客,这也符合技术人的实践理念. 最近,重温PHP开发实战权威指南,做点总结,整理下自己学习 ...

随机推荐

  1. SharpZipLib压缩解压的使用

    项目中使用 Velocity 将模板和生成的动态内容(HTML.XML等)合并保存到redis数据库中,考虑到压缩的文件容量会比较小,方便传输而且存储所使用的空间也会比较小,所以要压缩一下,读取的时候 ...

  2. 在source中查看代码

    如果你想查看网页的代码,有时会出现这种情况: o my god!啥都看不出来!这要怎么办?放弃吗,当然不啦! 点击这个神奇的按钮,奇迹发生了! 瞬间变成这个样子,哇,好激动!

  3. 【js】字符串方法

    let str = 'na'; console.log(str.repeat('3'));//nanana console.log(str.startsWith('n'));//true consol ...

  4. python defaultdict模块

    from collections import defaultdict '''默认值字典'''d = defaultdict(lambda :123)print(d)print(type(d))pri ...

  5. 记一次在咸鱼上购买 MacBook Pro 的经历

    前言 以前一直用的是 windows 的,但是最近特别想买个 macOS 的.其实不是为了其他什么目的,只是涉及到开发 macOS更接近 linux 系统,一直没使用过所以就想尝试体验下,而且现在很多 ...

  6. 发布时一键添加html中的css标签和script标签版本号来防止浏览器缓存

    AppendFileVersion 是一个VSIX插件支持vs2015意以上版本 是我用来发布时一键添加html中的css标签和script标签版本号来防止浏览器缓存 分享给大家! download ...

  7. 记录学习antd design pro dva的过程,主要记错, 多图预警,如有理解偏差,忘指出,多谢!

    首要问题: 如何增加菜单项 答案: 在router.config中添加路由,在locales语言国际化增加选项 问题1: 答案1: 问题2: 这个要修改state,正确写法 存在的疑惑:为什么不能直接 ...

  8. C#字符串和ASCII码的转换

    //字符转ASCII码: public static int Asc(string character) { if (character.Length == 1) { System.Text.ASCI ...

  9. 内核调试打印dump_stack

    https://blog.csdn.net/dragon101788/article/details/9419175 在函数中加入dump_stack函数 需要包含的头文件: #include < ...

  10. 华硕AC66U_B1救砖或者恢复固件

    手贱,刷了个DD-WRT,然后发现无法还原固件,刷不回原厂固件.网上找不到恢复的方法.还是DD-WRT官网给力,本身就提供了恢复原厂固件的方法,是英文的.但是其中提到的一个工具,现在没有了.ASUS ...