在没有Web存储(localStorage 和 sessionStorage)、 IndexedDB之前,客户端想要存储数据时,采用的是什么方式呢?
cookie是一种有效的方式,它是用户浏览器保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。
与其同时期能实现客户端数据存储的还有Flash。不过Flash存储随着时间的推移以及很多安全问题已经退出了历史的舞台。今天这篇文章主要学习的是cookie高级使用和注意事项,现在正式开始吧!
客户端读写Cookie
//读
cookieData = document.cookie;
//写
document.cookie = "testUid=test;path=/;";
//设置有效期
document.cookie="testUid=test;path=/;expires=${expiresTime];";
Cookie 的属性
客户端写Cookie注意点
- 删除 cookie ,设置cookie的过期时间为过去时间即可
- 设置多个 cookie 需要多次调用
document.cookie - 修改和删除cookie字段,要保障 path 和 domian 值不变
服务端写Cookie注意点
app.post('/login', (_req, res) => {
// 设置cookie
res.setHeader('Set-Cookie', ['userToken=1111;Max-Age=86400;']);
//或者
res.cookie("cid", '555', { maxAge: 86400, httpOnly: true });
});
服务端设置cookie信息,并在客户端请求时把这个cookie信息发送给客户端,客户端会自动保存cookie的key/value值。
Cookie的几个特殊选项
会话期 cookie
-
定义:浏览器会话期间的cookie,浏览器关闭以后自动删除(和 sessionStorage 类似)
-
设置会话期cookie:不指定过期时间(Expires)和有效期 (Max-Age) 即可
当你不想在浏览器中写入数据时,可以使用会话期 cookie,这样做也可以避免 cookie 被他人盗取和直接利用的问题。
持久化 cookie
- 定义:持久化cookie的生命周期取决于过期时间(Expires) 和有效期( Max-Age),持久化cookie按照一定编码规则(不是明文)存储在客户端硬盘中
- Max-Age:正数,cookie持久化时间,单位秒
- Max-Age:0,可以删除cookie
HttpOnly
httpOnly,它是一种安全标志,用于限制 Cookie 的访问权限
- 当它设置为 true 时,可以阻止通过 js 访问cookie,能有效的防止xSS攻击
- Document.cookie无法访问
Secure
Secure属性是一项重要的安全设置,用于保护Cookie的传输过程,防止被恶意截获和篡改
- 设置为true之后,浏览器仅在通过HTTPS协议发送请求时才会将Cookie包含在请求头中
- 如果未设置 Secure 属性或者设置为 false ,那么无论是通过 HTTP 还是 HTTPS 发送请求,Cookie 都会被包含在请求头中
Cookie 的作用域
域名介绍
-
公共域名后缀
com和org、net、tech都是公共后缀。要注意的是,
github.io也是公共的后缀。举个例子,a.github.io和b.github.io分别代表完全不同的项目主页且a 项目和 b 项目毫不相干,但是它们都共享github.io域名,因此github.io这种知名站点就成了公共后缀,a.github.io和b.github.io也被 samesite 规则认为是不同的网站。 -
子域名和父域名
域名是继承结构的,
x.itthink.tech是itthink.tech的下级域名,itthink.tech是x.itthink.tech的上级域名。每个域名是其所有下级域名的父域名,每个域名的下级域名都算是其子域名。 -
环境域名
对于在浏览器中运行的 JavaScript 来说,它的域名环境就是当前浏览器地址栏中访问的域名。对于一个Http请求来说,它的域名环境是指当前Http请求的URL中的域名。
有了域名概念的铺垫,我们再来理解一下的 Cookie 属性名词。
Domain
Domain 决定 Cookie 在哪个域是有效的。如 Doamin 设置为 .a.com,则 b.a.com 和 c.a.com 均可使用该 Cookie,但如果设置为 b.a.com,则 c.a.com 不可使用该 Cookie 。
Domain参数必须以点 . 开始。
Domain不可以共享其子域名和兄弟域名的Cookie。
Path
Path 是一个路径,代表只有当前访问URL的路径是 path 或其子路径时Cookie才可以被访问。
Path 和 Domain 共同限定了Cookie的作用范围,我们每次设置Cookie时,这两个值如果被忽视往往会带来意想不到的bug。当我们要修改或者删除一个Cookie时,也一定要记住必须指定和目标Cookie相同的 path 和 domain。
Samesite
Samesite 表示是否允许跨站请求时携带 Cookie 。也可以换一句话解释,samesite 表示是否允许当前Cookie作为第三方Cookie被发送到服务端。
举一个实际一点的例子来解释:
银行网站在浏览器端设置了一个用于身份认证的Cookie,钓鱼网站在浏览器端给银行网站的服务端发请求时,能否带上这个Cookie,这由该Cookie的 samesite 属性决定。
Samesite 属性和值都不区分大小写,samesite 属性可以有如下值: strict、 lax、none。
CSRF (跨站请求伪造)攻击
在详细讲解 samesite 的各个值之前,我们还需要了解一下CSRF(跨站请求伪造)攻击,以上面的银行的例子为基础,拓展讲解什么是 CSRF?为什么说 Samesite 能有效阻止 CSRF 攻击?
CSRF 攻击步骤:
- 某银行网站
bank.com在用户登录后会种一个用于身份认证的Cookie。 - 某钓鱼网站
bunk.com在研究了银行网站转账的Http接口后,在钓鱼网站上放了一个表单提交按钮,点击按钮会向该接口提交数据,接口的参数被设置为当前用户账户给钓鱼网站账户转账。 - 用户登录银行网站 bank.com ,并没有登出,而是继续访问了钓鱼网站 bunk.con 并点击了其表单提交按钮,此时Cookie会一并被发送到银行服务器,由于有Cookie存在,银行验证通过,钓鱼网站成功将用户的钱转入自己的账户,攻击顺利完成。
画图示例如下:
Samesite 的属性
1. strict
为了避免CSRF攻击,最有效的方法就是禁止在跨站请求中携带Cookie。samesite = strict 可以全面的防止跨站请求携带Cookie,让浏览器只在相同站点发送Cookie。
2. lax
新版浏览器默认选项,允许部分第三方请求携带cookie
- 浏览器地址跳转到Cookie所属网站且是 GET 请求时,会带上Cookie。
- 标签对即将要跳转的网页发送请求进行预渲染时,会带上Cookie。
3. none
浏览器不限制,同站和跨站都可以发送cookie
cookie 同源和同站区别
- 同源要求协议、端口、域名都要一致
- 同站要求有效顶级域名和二级域名保持一致,不考虑端口和协议
举个例子,a.taobao.com 和 b.taobao.com 就是同站, 12.0.0.1:8000 和 127.0.0.1:443 也是同站,http 和 https 也不用考虑。
注意:github.io 属于一个有效的顶级域名,所以 a.github.io 和 b.github.io 属于跨站。
SameSite 跨站携带cookie 的策略
下图是不同的请求遇到不同的 SameSite 属性值时,能够被允许跨站携带 Cookie 的情况分布列表:
| 请求类型 | 实例 | Strict跨站 | Lax跨站 | None跨站 |
|---|---|---|---|---|
| 链接 | <a href=".." /> | ❌ | ✅ | ✅ |
| 预加载 | <link href="" rel="prerender" /> | ❌ | ✅ | ✅ |
| get表单 | <form method="GET" action="" /> | ❌ | ✅ | ✅ |
| post表单 | <form method= "POST" action="" /> | ❌ | ❌ | ✅ |
| iframe | <iframe src="" /> | ❌ | ❌ | ✅ |
| AJAX | <img src="" /> | ❌ | ❌ | ✅ |
为了方便大家理解跨站携带 cookie 的概念,接下来举个小栗子进行演示:
1、准备工作
准备一个客户端,域名为127.0.0.1:660,该客户端有两个页面分别是
- 客户端url:
https://127.0.0.1:660/login.html - 客户端url:
https://127.0.0.1:660/test.html
准备一个域名为 test.web.com的服务端
- 服务端url:
https://test.web.com/index.html,
服务端接口代码:
app.post('/login',(_req,res)=>{
console. log('req: cookie', _req.headers['cookie']);
// 设置cookie,设置Samesite
res.cookie("ci55",'555',{
path:"/",
maxAge: 60 * 60 * 1000,
sameSite: 'none', //配合 secure 使用
});
return res.json({
REV:true,
DATA:{
msg:"成功"
}
});
});
app.post('/test',(_req,res)=>{
return res.json({
REV:true,
DATA:{
msg:"成功"
}
});
});
2、login.html发送请求
在客户端的 login.html 页面向服务端发送 login 请求,服务端test.web.com所在的域名下被成功种下 Cookie:
3、test.html发送请求
在客户端的 test.html 页面向服务端发送 test 请求,从上面的代码可看出,test接口并未设置 cookie ,但是在请求头中依旧携带了 cookie 信息,这就是完整的跨站携带 cookie 案例。
3、注意事项
- SameSite 必须和 Secure 同时设置才生效
- 前端站点和后端接口都为 https
在上面的案例的第 2 步,服务端 cookie 信息有两条, 分别是 a 和 ci55 。但是test接口请求头中却只有名为 ci55 的cookie信息,怎么把名为 a 的 cookie 信息也携带上呢?
这就需要在服务端将名为 a 的 cookie 信息的 SameSite 设置为 true 、Secure 设置为 None 后,客户端请求头就会也携带上 a 的 cookie 信息。
cookie key 和 value 编解码
document.cookie= `${key]=${value};path=/;`
我们在设置 cookie 时,key 或者 value 中出现分号,等号等特殊符号时,会影响解析操作。在写入 cookie 时使用 encodeURIComponent() 编码,在读取时使用 decodeURIComponent() 解码 可以有效避免解析错误的情况发生。
检查用户是否禁用cookie
window.navigator.cookieEnabled //true/false
编写cookie工具库
我们可以自己写一个小型工具库来 get、set、remove ,更方便的操作Cookie信息。
开始码代码之前先简单包装一下编解码函数
function encode(s) {
return encodeURIComponent(s);
}
function decode(s) {
return decodeURIComponent(s);
}
get
看一个 cookie 结构,分析规律:
一对 key/vaule 加一个分号,分号后用空格间隔下一对 key/vaule。
找到规律后,我们可以先拆分分号和空格的位置,再拆分等号的位置就可以拿到 cookie 值啦
function getCookieItem(key) {
let result = key ? undefined : {},
cookies = document.cookie ? document.cookie.split("; ") : [],
i = 0,
l = cookies.length;
for (; i < l; i++) {
let parts = cookies[i].split("="),
//取第一个等号前面的作为key
name = decode(parts.shift()),
cookie = parts.join("=");
if (key === name) {
result = decode(cookie);
break;
}
if (!key && cookie !== undefined) {
//key 未定义,返回全部的key和value对象
result[name] = decode(cookie);
}
}
return result;
}
set
setCookieltem 函数接收三个参数,分别是键、值、选项。在传入的选项参数中额外的处理了expires属性,判断传入的 expires 构造函数,对不同类型的 expires 统一度量设置。最后再把这些键值放在数组里 join 操作转字符串存储。
function setCookieItem(key, value, options = {}) {
if (!key) return false;
console.log(options);
let sExpires = "";
if (options.expires) {
switch (options.expires.constructor) {
case Number:
sExpires = options.expires === Infinity
? "; expires=Fri, 31 Dec 9999 23:59:59 GMT"
: "; max-age=" + options.expires;
break;
case String:
sExpires = "; expires=" + options.expires;
break;
case Date:
sExpires = "; expires=" + options.expires.toUTCString();
break;
}
}
window.document.cookie = [
encode(key),
"=",
encode(value),
sExpires,
options.path ? "; path=" + options.path : "",
options.domain ? "; domain=" + options.domain : "",
options.secure ? "; secure" : "",
].join("");
return true;
}
remove
想要删除一个 cookie,很简单😎,利用 setCookieItem 设置过期时间即可
function removeCookieItem(key, options) {
setCookieItem(key, "", { ...options, expires: -1 });
return !getCookieItem(key);
}
完整代码看这里
新异步操作CookieAPl-CookieStore
通过上面的 cookie 工具库可以发现,整个新增和查阅的方法还是有些繁琐的,又要拆解又要拼接。有什么好的 API 能直接避免这些复杂的封装吗?
CookieStore!现在有了更方便操作 cookie 的方法 cookieStore ,这个方法是在 Chrome87 版本加入的,不过兼容性还不太好。
认识CookieStore
注意🥸:cookieStore 只能在https 协议下的域名才能访问的到;其他 http 协议的域名里会提示 cookieStore 为 undefined,或者设置失败。
设置 cookie
cookieStore.set 方法可以设置 cookie,并返回一个 Promise 状态,表示是否设置成功。
cookieStore
.set('username', 'wenzi')
.then(() => console.log('设置username成功'))
.catch(() => console.error('设置username失败'));
设置更多的属性时,可以传入一个 Object 类型:
cookieStore
.set({
name: 'age',
value: 18,
expires: new Date().getTime() + 24 * 60 * 60 * 1000,
})
.then(() => console.log('设置age成功'))
.catch(() => console.error('设置age失败'));
获取 cookie
cookieStore.get(name) 方法可以获取 name 对应的 cookie,会以 Promise 格式返回所有的属性:
获取所有的 cookie
cookieStore.getAll() 方法可以获取当前所有的 cookie
删除 cookie
cookieStore.delete(name) 用来删除指定的 cookie
cookieStore
.delete('age')
.then(() => console.log('删除age成功'))
.catch(() => console.error('删除age失败'));
监听 cookie 的变化
添加change事件,来监听 cookie 的变化,无论是通过 cookieStore 操作,还是直接操作 document.cookie,都能监听。
cookieStore.addEventListener('change', (event) => {
const type = event.changed.length ? 'change' : 'delete';
const data = (event.changed.length ? event.changed : event.deleted).map((item) => item.name);
console.log(`刚才进行了 ${type} 操作,cookie有:${JSON.stringify(data)}`);
});
cookieStore.set('math', 90);
拓展:
监听事件 event 有两个属性,一个是 changed,一个是 deleted。
-
调用 set() 方法时,会触发 change 事件,被影响的 cookie 会放在
changed数组中。每次设置 cookie 时,即使两次的 name 和 value 完全一样,也会触发change事件。 -
通过 delete() 方法删除一个存在的 cookie 时,会触发 change 事件,被删除的 cookie 会放在
deleted数组中。如果删除一个不存在的 cookie,则不会触发 change 事件。 -
通过
document.cookie设置或者删除的 cookie,均认为是在修改 cookie,而不是删除。修改 cookie
删除 cookie
现在通过一个完整的小demo来了解 CookieStore 的使用以及不同写法吧😎! 以上代码建议大家复制出来,在本地运行进行调试,效果会更好喔👀
CookieStore - service worker中使用
以上代码建议大家复制出来,在本地运行进行调试,效果会更好喔👀
CookieStore-注意事项
- 安全上下文中可用,比如https,localhost等
- 返回的都是Promise
- Firefox 和 Safari 不支持