Cookie 简介
HTTP 协议是无状态的,但可以通过 Cookie 来维持客户端与服务端之间的“会话状态”。
简单来说就是:服务端通过 Set-Cookie 响应头设置 Cookie 到客户端,而客户端在下次向服务器发送请求时添加名为 Cookie 的请求头,以携带服务端之前“埋下”的内容,从而使得服务端可以识别客户端的身份。
举个简单的?:
// 服务端
const http = require("http");
http
.createServer((req, res) => {
if (req.url == "/") {
res.end("hello world");
} else if (req.url == "/favicon.ico") {
res.statusCode = 204;
res.end();
} else {
res.writeHead(200, [
["Set-Cookie", "name=haochuan9421"], // 设置 cookie
]);
res.end("some data");
}
})
.listen(80);
// 客户端
var xhr = new XMLHttpRequest();
xhr.open('GET', "/someapi");
xhr.send();
当客户端再次发起请求时就会自动携带上之前“埋下”的 Cookie:
简单的介绍完 Cookie 后,我们来看一下它的 SameSite
属性。
SameSite 属性
SameSite
有三个可选值:
Strict
Lax
None
。
从 Chrome 80 开始,如果不指定 SameSite 就等效于设置为 Lax
。你可以通过 chrome://flags/#same-site-by-default-cookies 禁用这个行为,禁用后不指定 SameSite 就等效于设置为 None
。关于他们的区别我们稍后结合具体的场景来介绍。
先来看看上图中出现的 third-party
这个概念,对 Cookie 来说什么是第三方 呢,"SameSite 同站" 又是什么意思呢?
举个例子:假设我们的网站是 bar.com ,当我们引入 foo.com 的图片时,图片服务如果设置了 cookie,我们就称之为”第三方 cookie“。
是否是 “第三方” 不是根据同源策略(协议,主机,端口)来判断,而是 PSL(公共后缀列表),比如 'bar.com' 和 'a.bar.com' 就不是 “第三方” 的关系,而是“同站”。检测两个域名是否是同站的方法也很简单,比如你在 'a.bar.com' 网站,设置 document.domain = 'bar.com'
并不会报错,但如何设置 document.domain = 'foo.com'
就会报错,那么 'foo.com' 相对于 'a.bar.com' 来说就是第三方。
当端口不同时,比如我们的网站是 bar.com:8080 ,我们引入 bar.com:9000 的图片时不会判定为第三方的
但是协议不同默认会判定为第三方。比如我们的网站是 bar.com ,我们引入 bar.com 的图片时会判定为第三方。不过在 Chrome 中你可以通过 chrome://flags/#schemeful-same-site 来忽略协议的限制。
除了图片这种场景,向第三方网站发起 AJAX/fetch 请求、嵌入第三方网站的 iframe、表单提交到第三方网站、链接跳转到第三方网站等都可能涉及到“第三方 cookie”。针对这些可能出现 “第三方cookie” 的场景,SameSite 设置为不同的值又会有哪些不同的效果呢?让我们来一一探究(多图警告?):
1. AJAX 请求
当我们跨域发送 AJAX 请求时,由于浏览器同源策略的限制,我们的请求是无法发送的:
不过我们可以使用 CORS 的方式来解决跨域的问题:
const http = require("http");
http
.createServer((req, res) => {
if (req.url == "/") {
res.end("hello world");
} else if (req.url == "/favicon.ico") {
res.statusCode = 204;
res.end();
} else {
res.writeHead(200, [
["Set-Cookie", "name=haochuan9421"], // 设置 cookie
["Access-Control-Allow-Origin", "*"], // 允许跨域请求
]);
res.end("some data");
}
})
.listen(80, "0.0.0.0");
但是当我们再次发起请求时,虽然这个跨域请求的响应头中有设置 Cookie,却发现下次请求时并不会携带之前服务器设置的 Cookie。
这就带来一个问题,我们失去了利用 Cookie 来维持服务端与客户端“会话状态”的能力。那么如何在向第三方网站请求的时候携带 Cookie 呢?需要满足如下条件:
- 网站开启 https 并将 Cookie 的 Secure 属性设置为 true
- Access-Control-Allow-Origin 设置为具体的 origin,而不是 *
- Access-Control-Allow-Credentials 设置为 true
- SameSite 属性设置为 None
const https = require("https");
const fs = require("fs");
https
.createServer(
{
key: fs.readFileSync(__dirname + "/key.pem"),
cert: fs.readFileSync(__dirname + "/cert.pem"),
},
(req, res) => {
if (req.url == "/") {
res.end("hellow world");
} else if (req.url == "/favicon.ico") {
res.statusCode = 204;
res.end();
} else {
res.writeHead(200, [
["Set-Cookie", "name=haochuan9421; Secure; SameSite=None"],
...(req.headers.origin // 跨域请求时请求头中会包含 origin,也就是请求发出的网站
? [
["Access-Control-Allow-Origin", req.headers.origin], // 不可以使用 *,必须指定
["Access-Control-Allow-Credentials", "true"], // 设置允许跨域请求携带 Cookie
]
: []),
]);
res.end("some data");
}
}
)
.listen(443, "0.0.0.0");
满足上面的条件之后,跨域请求就可以携带 Cookie 了:
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.open('GET', "https://www.bar.com/someapi");
xhr.send();
这四个条件缺一不可:
当不开启 https 的时候:
当不设置 Secure 属性:
当 Access-Control-Allow-Origin 设置为 * 时
当 Access-Control-Allow-Credentials 的值不为 true 时
当 SameSite 属性设置为 Strict 或 Lax 时
对于使用浏览器的 fetch API 发送请求也是一样的,使用 fetch 发起跨域请求时如果想携带 cookie,需要设置 "credentials" 为 "include":
fetch("https://www.bar.com/somedata", {
"method": "GET",
"credentials": "include"
})
2. 嵌套第三方 iframe
const https = require("https");
const fs = require("fs");
https
.createServer(
{
key: fs.readFileSync(__dirname + "/key.pem"),
cert: fs.readFileSync(__dirname + "/cert.pem"),
},
(req, res) => {
console.log(req.headers.host);
if (req.url == "/") {
if (req.headers.host === "www.foo.com") {
res.setHeader("Content-Type", "text/html;charset=utf-8");
res.end(`<div>这是父页面</div>
<iframe src="https://www.bar.com/"></iframe>`);
} else {
res.writeHead(200, [
["Set-Cookie", "name=haochuan9421; Secure; SameSite=None"],
["Content-Type", "text/html;charset=utf-8"],
]);
res.end(`<div>这是子页面</div>`);
}
} else {
res.statusCode = 204;
res.end();
}
}
)
.listen(443, "0.0.0.0");
如果设置了SameSite 为 Strict: 如果设置了SameSite 为 Lax: 如果不指定 SameSite: 如果设置了 SameSite 为 None:
这说明只有明确的指定了 SameSite 为 None 时,跨域 iframe 页面被引入时 Cookie 才能生效。
举例说明一下:假设我们希望在自己的网站内嵌 bilibili 的视频播放器,直接通过 iframe 把 B 站播放器引入到我们自己的网站是无法使用 1080p 画质的。
<iframe
src="//player.bilibili.com/player.html?bvid=BV1Vv41157uK&high_quality=1"
allowfullscreen="allowfullscreen"
width="100%"
height="500"
scrolling="no"
frameborder="0"
></iframe>
这是由于 B 站 Cookie 的 SameSite 属性并没有设置为 None,内嵌在其他第三方网站时 B 站播放器无法传递 Cookie 到服务器,服务器也就拿不到用户的登录态,对于未登录的用户 B 站是不提供 1080p 播放的。
不过在 Chrome 中我们可以通过禁用 chrome://flags/#same-site-by-default-cookies 来让”第三方 cookie“默认为 None,当我们关闭这个选项并重启浏览器之后,就可以在内嵌 iframe 中播放 1080p 的 B站视频了(前提是在 B 站已经登录过)。
3. 加载第三方图片或脚本等
const https = require("https");
const fs = require("fs");
https
.createServer(
{
key: fs.readFileSync(__dirname + "/key.pem"),
cert: fs.readFileSync(__dirname + "/cert.pem"),
},
(req, res) => {
console.log(req.headers.host, req.url);
if (req.url == "/") {
if (req.headers.host === "www.foo.com") {
res.setHeader("Content-Type", "text/html;charset=utf-8");
res.end(`<div>这是父页面</div>
<img src="https://www.bar.com/"></img>`);
} else {
res.writeHead(200, [
["Set-Cookie", "name=haochuan9421; Secure; SameSite=Strict"],
["Content-Type", "image/png"],
]);
fs.createReadStream("logo.png").pipe(res);
}
} else {
res.statusCode = 204;
res.end();
}
}
)
.listen(443, "0.0.0.0");
这和引入第三方的 iframe 是一样的,只有 SameSite 属性为 None,Cookie 才能生效。
举个应用的例子:下图是一个添加了谷歌广告的网站,可以看到谷歌广告相关的 Cookie 会把 SameSite 属性设置为 None。这样当足够多的网站引入了谷歌的广告脚本等资源时,他就可以构建出用户在各个网站的浏览轨迹以及访问偏好了,从而精准的推送广告。
4. 提交表单到第三方网站
const https = require("https");
const fs = require("fs");
https
.createServer(
{
key: fs.readFileSync(__dirname + "/key.pem"),
cert: fs.readFileSync(__dirname + "/cert.pem"),
},
(req, res) => {
if (req.url == "/") {
if (req.headers.host === "www.foo.com") {
res.setHeader("Content-Type", "text/html;charset=utf-8");
res.end(`<form action="https://www.bar.com/" method="post" enctype="multipart/form-data">
<input type="text" name="name" />
<input type="number" name="age" />
<button type="submit">提交</button>
</form>`);
} else {
console.log(req.headers.host, req.url, req.method, req.headers.cookie);
res.writeHead(200, [
["Set-Cookie", "name=haochuan9421; Secure; SameSite=Strict"],
]);
res.end("ok");
}
} else {
res.statusCode = 204;
res.end();
}
}
)
.listen(443, "0.0.0.0");
从上面的测试中可以看出将 SameSite 设置为 None 是一种危险的行为,它会使得针对你的网站发起 CSRF (Cross-site request forgery) 攻击变得非常容易,因为从一个第三方恶意网站向你的网站发起的请求也会携带 Cookie,这使得伪造的请求会被识别为一次普通用户发起的请求。下面具体演示一下,我们假设 www.foo.com 是一个恶意网站,www.bar.com 是我们自己的网站:
// 这是我们自己正常的网站
const https = require("https");
const fs = require("fs");
https
.createServer(
{
key: fs.readFileSync(__dirname + "/key.pem"),
cert: fs.readFileSync(__dirname + "/cert.pem"),
},
(req, res) => {
if (req.url == "/") {
// 我们网站首页有一个转账的表单
res.setHeader("Content-Type", "text/html;charset=utf-8");
res.end(`<form action="/transfer" method="post">
<input type="number" name="money" />
<button type="submit">提交</button>
</form>`);
} else if (req.url == "/login") {
// 登录后,客户端会存储用户的 Cookie 信息
res.setHeader("Set-Cookie", "name=haochuan9421; Secure; SameSite=None");
res.end("login success");
} else if (req.url == "/transfer") {
// 登录后的用户可以转账,未登录的不能转账
res.end(req.headers.cookie ? "ok" : "fail");
} else {
res.statusCode = 204;
res.end();
}
}
)
.listen(443, "0.0.0.0");
用户直接访问 www.bar.com 提交表单转账,由于没有登录(没有 Cookie)会提示失败,所以用户会先进入 www.bar.com/login 登录,登录后客户端会有 Cookie,当用户回到首页再次提交转账表单时,就会转账成功,这模拟了一个简单的基于 Cookie 鉴权的网站。
接下来我们一起来看看攻击者是如何突破 www.bar.com 的鉴权滴。当攻击者知道了你网站有转账的功能,那么他就可以诱导用户进入准备好的恶意网站,在这个恶意网站中向你的网站发起转账请求,如果进入恶意网站的用户之前登录过你的网站并且登录态没有过期,那么这次伪造的请求就会成功把用户的钱转走。下面是恶意网站的代码:
// 这是一个要伪造请求的恶意网站
const https = require("https");
const fs = require("fs");
https
.createServer(
{
key: fs.readFileSync(__dirname + "/key.pem"),
cert: fs.readFileSync(__dirname + "/cert.pem"),
},
(req, res) => {
if (req.url == "/") {
res.setHeader("Content-Type", "text/html;charset=utf-8");
res.end(`<div>这是一个恶意网站</div>
<form
id="fake-form"
action="https://www.bar.com/transfer"
method="post"
target="submit-target"
>
<input type="hidden" name="money" value="1000" />
</form>
<iframe name="submit-target"></iframe>
<script>document.getElementById("fake-form").submit();</script>`);
} else {
res.statusCode = 204;
res.end();
}
}
)
.listen(443, "0.0.0.0");
可以看到,用户被诱导进入恶意网站后,恶意网站自动像你的服务器发起了伪造的转账请求,由于你 Cookie 中的 SameSite 属性设置为 None,这就导致这次伪造的请求也会携带用户的 Cookie,单纯基于 Cookie 做的接口鉴权就被攻破了,用户的资金面临安全风险。这也是为什么最新版的浏览器都会把 SameSite 的默认值从 None 调整为 Lax 的一个重要原因。
5. 链接跳转第三方网站
const https = require("https");
const fs = require("fs");
https
.createServer(
{
key: fs.readFileSync(__dirname + "/key.pem"),
cert: fs.readFileSync(__dirname + "/cert.pem"),
},
(req, res) => {
if (req.url == "/") {
if (req.headers.host === "www.foo.com") {
res.setHeader("Content-Type", "text/html;charset=utf-8");
res.end(`<div>foo page</div>
<a href="https://www.bar.com/">www.bar.com</a>`);
} else {
console.log(req.headers.host, req.url, req.headers.cookie);
res.writeHead(200, [
["Set-Cookie", "name=haochuan9421; Secure; SameSite=None"],
["Content-Type", "text/html;charset=utf-8"],
]);
res.end("bar page");
}
} else {
res.statusCode = 204;
res.end();
}
}
)
.listen(443, "0.0.0.0");
Strict
这个规则过于严格,可能造成非常不好的用户体验。比如,当前网页有一个 GitHub 链接,用户点击跳转就不会带有 GitHub 的 Cookie,跳转过去总是未登陆状态。
总结
现代浏览器针对 Cookie 的 SameSite
属性的默认值已经很合理了,作为网站所有者通常不需要手动设置这个属性,一般只有当我们的服务需要和“第三方”对接时才考虑怎么设置更合理。
Strict
最为严格,表示完全禁止“第三方 Cookie”,只有当前网页的 URL 与请求目标一致时,才会带上 Cookie,一般用于保证系统的封闭性和安全性。
Lax
是目前大多数现代浏览器的默认值,他在保证安全性的前提下,也可以避免一些不好的用户体验,比如从别的网站跳转过时会没有登录态。
None
是最为宽松的一种设定,通常用于开放我们的服务给不同的第三方接入,同时又需要追踪用户的场景,比如广告,设置为 None
时需要考虑开放的安全性。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!