记一场与 cookie 的相遇

时间:2021-02-14 00:36:20

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

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

我们通过一张图来说明下 cookie 的原理:

记一场与 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