关于 HTTP cookie 的实用完整教程
在 Web 开发中,cookie 是什么?
Cookie 是后端可以存储在用户浏览器中的微小数据片段。用户跟踪、个性化以及最重要的身份验证是 Cookie 最常见的用例。
Cookie 存在很多隐私问题,多年来一直受到严格的监管。
在这篇文章中,我将主要关注技术方面:您将学习如何在前端和后端创建、使用和处理 HTTP cookie。
您将学到什么
在以下指南中您将了解:
- 如何使用 cookies、后端和前端
- Cookie安全和权限
- Cookie、AJAX 和 CORS之间的交互
设置后端
后端的示例是用 Flask 编写的 Python 代码。如果你想继续学习,请创建一个新的 Python 虚拟环境,进入该环境,然后安装 Flask:
mkdir cookies && cd $_
python3 -m venv venv
source venv/bin/activate
pip install Flask
在项目文件夹中创建一个名为的新文件flask_app.py
,并使用我的示例进行本地实验。
谁创建了 cookies?
首先,Cookie 从何而来?谁创建了 Cookie?
虽然可以使用在浏览器中创建 cookie document.cookie
,但大多数情况下,后端有责任在将响应发送到客户端之前在响应中设置 cookie。
这里所说的后端是指可以通过以下方式创建 cookie:
- 后端的实际应用程序代码(Python、JavaScript、PHP、Java)
- 响应请求的网络服务器(Nginx、Apache)
为此,后端在响应中设置一个 HTTP 标头,该标头Set-Cookie
以由键/值对组成的相应字符串命名,并附加可选属性:
Set-Cookie: myfirstcookie=somecookievalue
何时何地创建这些 cookie 取决于要求。
所以,Cookie就是简单的字符串。考虑使用 Flask 在 Python 中实现的这个例子。flask_app.py
在项目文件夹中创建一个名为 的 Python 文件,并写入以下代码:
from flask import Flask, make_response
app = Flask(__name__)
@app.route("/index/", methods=["GET"])
def index():
response = make_response("Here, take some cookie!")
response.headers["Set-Cookie"] = "myfirstcookie=somecookievalue"
return response
然后运行该应用程序:
FLASK_ENV=development FLASK_APP=flask_app.py flask run
当此应用程序运行时,用户访问http://127.0.0.1:5000/index/后端会设置一个以键/值对命名的响应头。Set-Cookie
(127.0.0.1:5000是开发中Flask应用程序的默认监听地址/端口)。
标Set-Cookie
头是了解如何创建 Cookie 的关键:
response.headers["Set-Cookie"] = "myfirstcookie=somecookievalue"
在右侧你可以看到实际的 cookie "myfirstcookie=somecookievalue"
。
大多数框架都有自己的实用函数,用于以编程方式设置 cookie,例如Flask 的 set_cookie()
。
在底层,他们只是在响应中设置了一个标题Set-Cookie
。
如何查看cookie?
再次回顾前面使用 Flask 的示例。访问http://127.0.0.1:5000/index/document.cookie
后,后端会在浏览器中设置一个 Cookie。要查看此 Cookie,您可以从浏览器控制台调用:
或者,您可以检查开发者工具中的“存储”选项卡。点击“Cookies”,您应该在那里看到 cookie:
在命令行上,您还可以使用curl来查看后端设置的 cookie:
curl -I http://127.0.0.1:5000/index/
要将 Cookie 保存到文件以供日后使用:
curl -I http://127.0.0.1:5000/index/ --cookie-jar mycookies
要在标准输出上显示 cookie:
curl -I http://127.0.0.1:5000/index/ --cookie-jar -
请注意,不带该HttpOnly
属性的 Cookie 可以通过浏览器中的 JavaScript 访问document.cookie
。另一方面,标记为 的 Cookie 则HttpOnly
无法通过 JavaScript 访问。
要将 Cookie 标记为HttpOnly
传递 Cookie 中的属性:
Set-Cookie: myfirstcookie=somecookievalue; HttpOnly
现在该 cookie 仍会出现在 Cookie 存储选项卡中,但document.cookie
会返回一个空字符串。
从现在开始,为了方便起见,我将使用 Flask response.set_cookie()
在后端创建 cookie。
为了在本指南中检查 Cookie,我们将使用以下方法:
- 卷曲
- Firefox 开发者工具
- Chrome 开发者工具
我得到了一块饼干,现在怎么办?
你的浏览器获取了一个 Cookie。接下来怎么办?一旦获取到 Cookie,浏览器就可以将其发送回后端。
这可能有许多应用:用户跟踪、个性化,以及最重要的身份验证。
例如,一旦你登录网站,后端就可以给你一个 cookie:
Set-Cookie: userid=sup3r4n0m-us3r-1d3nt1f13r
为了在每个后续请求中正确识别您,后端会检查请求中来自浏览器的 cookie。
为了发送 cookie,浏览器会Cookie
在请求中附加一个标头:
Cookie: userid=sup3r4n0m-us3r-1d3nt1f13r
浏览器如何、何时以及为何发回 Cookie是下一节的主题。
Cookies 会过期:Max-Age 和 expires
默认情况下,Cookie 会在用户关闭会话(即关闭浏览器)时过期。为了持久化 Cookie,我们可以传递expires
或Max-Age
属性:
Set-Cookie: myfirstcookie=somecookievalue; expires=Tue, 09 Jun 2020 15:46:52 GMT; Max-Age=1209600
当存在机器人属性时,Max-Age
优先于expires
。
Cookie 的作用域由路径决定:Path 属性
考虑一下这个后端,它在访问http://127.0.0.1:5000/时为其前端设置了一个新的 cookie 。在其他两个路由上,我们打印请求的 cookie:
from flask import Flask, make_response, request
app = Flask(__name__)
@app.route("/", methods=["GET"])
def index():
response = make_response("Here, take some cookie!")
response.set_cookie(key="id", value="3db4adj3d", path="/about/")
return response
@app.route("/about/", methods=["GET"])
def about():
print(request.cookies)
return "Hello world!"
@app.route("/contact/", methods=["GET"])
def contact():
print(request.cookies)
return "Hello world!"
要运行该应用程序:
FLASK_ENV=development FLASK_APP=flask_app.py flask run
在另一个终端中,如果我们与根路由建立连接,我们可以看到 cookie Set-Cookie
:
curl -I http://127.0.0.1:5000/ --cookie-jar cookies
HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 23
Set-Cookie: id=3db4adj3d; Path=/about/
Server: Werkzeug/1.0.1 Python/3.8.3
Date: Wed, 27 May 2020 09:21:37 GMT
注意 cookies 有一个Path
属性:
Set-Cookie: id=3db4adj3d; Path=/about/
现在让我们通过发送第一次访问时保存的 cookie 来访问 /about/ 路由:
curl -I http://127.0.0.1:5000/about/ --cookie cookies
在 Flask 应用程序运行的终端中,您应该看到:
ImmutableMultiDict([('id', '3db4adj3d')])
127.0.0.1 - - [27/May/2020 11:27:55] "HEAD /about/ HTTP/1.1" 200 -
正如预期的那样,cookie 返回到了后端。现在尝试访问 /contact/ 路由:
curl -I http://127.0.0.1:5000/contact/ --cookie cookies
这次在 Flask 应用程序运行的终端中您应该看到:
ImmutableMultiDict([])
127.0.0.1 - - [27/May/2020 11:29:00] "HEAD /contact/ HTTP/1.1" 200 -
这是什么意思?Cookie 的作用域受路径限制。具有指定属性的 Cookie Path
不能发送到另一个不相关的路径,即使这两个路径位于同一个域中。
这是cookie的第一层权限。
当Path
在创建 cookie 期间省略时,浏览器默认为/。
Cookie 的范围由域决定:Domain 属性
Domain
Cookie 属性的值控制浏览器是否接受它以及Cookie返回到哪里。
让我们看一些例子。
注意:以下 URL 是在免费的 Heroku 实例上。请稍等片刻,等待其启动。打开链接之前,请先打开浏览器的控制台,以便在网络选项卡中查看结果。
主机不匹配(错误的主机)
考虑以下由https://serene-bastion-01422.herokuapp.com/get-wrong-domain-cookie/ 设置的 cookie:
Set-Cookie: coookiename=wr0ng-d0m41n-c00k13; Domain=api.valentinog.com
这里的 cookie 源自serene-bastion-01422.herokuapp.com,但Domain
属性有api.valentinog.com。
浏览器没有其他选择来拒绝此 Cookie。例如,Chrome 会发出警告(Firefox 不会):
不匹配的主机(子域名)
考虑以下由https://serene-bastion-01422.herokuapp.com/get-wrong-subdomain-cookie/ 设置的 cookie:
Set-Cookie: coookiename=wr0ng-subd0m41n-c00k13; Domain=secure-brushlands-44802.herokuapp.com
这里的 cookie 源自serene-bastion-01422.herokuapp.com,但Domain
属性是secure-brushlands-44802.herokuapp.com。
它们位于同一个域名,但子域名不同。同样,浏览器也会拒绝此 Cookie:
匹配主机(整个域)
现在考虑通过访问 https://www.valentinog.com/get-domain-cookie.html 设置的以下 cookie :
set-cookie: cookiename=d0m41n-c00k13; Domain=valentinog.com
此 cookie 是在 Web 服务器级别使用Nginx add_header设置的:
add_header Set-Cookie "cookiename=d0m41n-c00k13; Domain=valentinog.com";
这里我使用 Nginx 来演示设置 Cookie 的各种方法。Cookie 是由 Web 服务器设置还是由应用程序代码设置,对浏览器来说并不重要。
重要的是 cookie 来自哪个域。
这里浏览器会很乐意接受该 cookie,因为中的主机Domain
包含了该 cookie 所来自的主机。
换句话说,valentinog.com 包含子域名www.valentinog.com。
此外,任何针对 valentinog.com 的新请求以及任何针对 valentinog.com 子域的请求都会返回此 cookie。
以下是对 www 子域的附加了 cookie 的请求:
以下是对另一个子域的请求,其中自动附加了 cookie:
Cookies 和公共后缀列表
现在考虑由 https://serene-bastion-01422.herokuapp.com/get-domain-cookie/ 设置的以下 cookie :
Set-Cookie: coookiename=d0m41n-c00k13; Domain=herokuapp.com
这里的 Cookie 来自serene-bastion-01422.herokuapp.com,属性Domain
是herokuapp.com。浏览器在这里应该做什么?
您可能认为 serene-bastion-01422.herokuapp.com 包含在域 herokuapp.com 中,因此浏览器应该接受该 cookie。
相反,它拒绝该 cookie,因为它来自公共后缀列表中包含的域。
公共后缀列表是由 Mozilla 维护的列表,所有浏览器都使用它来限制谁可以代表其他域设置 cookie。
资源:
匹配主机(子域名)
现在考虑由https://serene-bastion-01422.herokuapp.com/get-subdomain-cookie/设置的以下 cookie :
Set-Cookie: coookiename=subd0m41n-c00k13
当Domain
在 cookie 创建过程中省略时,浏览器会默认使用地址栏中的原始主机,在这种情况下我的代码会执行以下操作:
response.set_cookie(key="coookiename", value="subd0m41n-c00k13")
当 Cookie 进入浏览器的 Cookie 存储时,我们会看到Domain
应用的内容:
所以我们从 serene-bastion-01422.herokuapp.com 获取了此 cookie。现在应该将此 cookie 发送到哪里?
如果您访问https://serene-bastion-01422.herokuapp.com/,cookie会随请求一起出现:
但是,如果您访问 herokuapp.com,cookie 根本不会离开浏览器:
(herokuapp.com 稍后重定向到 heroku.com 并不重要)。
回顾一下,浏览器使用以下启发式方法来决定如何处理 cookie(这里的发送方主机是指您访问的实际 URL):
- 如果域或子域与
Domain
发送方主机不匹配,则完全拒绝该 cookie - 如果值
Domain
包含在公共后缀列表中,则拒绝该 cookie - 如果域或子域与
Domain
发送者主机匹配,则接受 cookie
一旦浏览器接受了 cookie,它就会发出请求,它会说:
- 如果请求主机与我在
Domain
- 如果请求主机是与我在
Domain
- 如果请求主机是包含在
Domain
类似 example.dev中的子域(如 sub.example.dev) ,则将其发回 cookie - 如果请求主机是主域(例如 example.dev)并且
Domain
是 sub.example.dev,则不要将其发回 cookie
要点:是与属性一起对 cookie 的Domain
第二层权限Path
。
Cookies 可以通过 AJAX 请求传输
Cookie 可以通过 AJAX 请求进行传输。AJAX请求是使用 JavaScript(XMLHttpRequest 或 Fetch)发起的异步 HTTP 请求,用于获取数据并将其发送回后端。
考虑另一个使用 Flask 的示例,其中我们有一个模板,该模板会加载一个 JavaScript 文件。以下是 Flask 应用:
from flask import Flask, make_response, render_template
app = Flask(__name__)
@app.route("/", methods=["GET"])
def index():
return render_template("index.html")
@app.route("/get-cookie/", methods=["GET"])
def get_cookie():
response = make_response("Here, take some cookie!")
response.set_cookie(key="id", value="3db4adj3d")
return response
这是模板templates/index.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<button>FETCH</button>
</body>
<script src="{{ url_for('static', filename='index.js') }}"></script>
</html>
以下是 JavaScript 代码static/index.js
:
const button = document.getElementsByTagName("button")[0];
button.addEventListener("click", function() {
getACookie();
});
function getACookie() {
fetch("/get-cookie/")
.then(response => {
// make sure to check response.ok in the real world!
return response.text();
})
.then(text => console.log(text));
}
访问http://127.0.0.1:5000/时,我们会看到一个按钮。点击该按钮后,我们向 /get-cookie/ 发出 Fetch 请求,以获取 cookie。正如预期的那样,该 cookie 被保存在浏览器的 Cookie 存储中。
现在让我们稍微改变一下 Flask 应用程序来公开另一个端点:
from flask import Flask, make_response, request, render_template, jsonify
app = Flask(__name__)
@app.route("/", methods=["GET"])
def index():
return render_template("index.html")
@app.route("/get-cookie/", methods=["GET"])
def get_cookie():
response = make_response("Here, take some cookie!")
response.set_cookie(key="id", value="3db4adj3d")
return response
@app.route("/api/cities/", methods=["GET"])
def cities():
if request.cookies["id"] == "3db4adj3d":
cities = [{"name": "Rome", "id": 1}, {"name": "Siena", "id": 2}]
return jsonify(cities)
return jsonify(msg="Ops!")
另外,让我们调整一下 JavaScript 代码,以便在获取 cookie 后发出另一个 Fetch 请求:
const button = document.getElementsByTagName("button")[0];
button.addEventListener("click", function() {
getACookie().then(() => getData());
});
function getACookie() {
return fetch("/get-cookie/").then(response => {
// make sure to check response.ok in the real world!
return Promise.resolve("All good, fetch the data");
});
}
function getData() {
fetch("/api/cities/")
.then(response => {
// make sure to check response.ok in the real world!
return response.json();
})
.then(json => console.log(json));
}
访问http://127.0.0.1:5000/时,我们会看到一个按钮。点击按钮后,我们会向 /get-cookie/ 发出一个 Fetch 请求,以获取 cookie。获取到 cookie 后,我们会立即向 /api/cities/ 发出另一个 Fetch 请求。
在浏览器的控制台中,你应该看到一个城市数组。此外,在开发者工具的“网络”选项卡中,你应该看到一个名为 的标头Cookie
,它通过 AJAX 请求传输到后端:
只要前端与后端处于相同的上下文中,前端和后端之间的 cookie 交换就可以正常工作:我们说它们位于同一来源。
这是因为默认情况下,Fetch仅当请求到达与请求触发相同的来源时才会发送凭据,即 cookie 。
这里,JavaScript 由http://127.0.0.1:5000/上的 Flask 模板提供。
让我们看看不同来源会发生什么。
Cookie 并不总是能够通过 AJAX 请求传输
考虑后端独立运行的不同情况,因此您运行此 Flask 应用程序:
FLASK_ENV=development FLASK_APP=flask_app.py flask run
现在在 Flask 应用程序之外的另一个文件夹中创建一个index.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<button>FETCH</button>
</body>
<script src="index.js"></script>
</html>
在同一文件夹中创建一个名为index.js
以下代码的 JavaScript 文件:
const button = document.getElementsByTagName("button")[0];
button.addEventListener("click", function() {
getACookie().then(() => getData());
});
function getACookie() {
return fetch("http://localhost:5000/get-cookie/").then(response => {
// make sure to check response.ok in the real world!
return Promise.resolve("All good, fetch the data");
});
}
function getData() {
fetch("http://localhost:5000/api/cities/")
.then(response => {
// make sure to check response.ok in the real world!
return response.json();
})
.then(json => console.log(json));
}
在同一文件夹中,从终端运行:
npx serve
此命令会提供一个本地地址/端口供您连接,例如http://localhost:42091/
。访问该页面,并在浏览器控制台打开的情况下尝试点击按钮。在控制台中,您应该看到:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:5000/get-cookie/. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing)
现在,http://localhost:5000/
与 不同http://localhost:42091/
。 它们的来源不同,因此CORS生效。
源由方案、域和端口号组成。这意味着http://localhost:5000/
和 是不同的源http://localhost:42091/
。
处理 CORS
CORS 是跨域资源共享的缩写,当在不同源上运行的 JavaScript 代码请求这些资源时,服务器可以控制对给定源上的资源的访问的一种方式。
默认情况下,浏览器会阻止对非同一来源的远程资源的 AJAX 请求,除非Access-Control-Allow-Origin
服务器公开了特定的 HTTP 标头。
为了修复第一个错误,我们需要为 Flask 配置 CORS:
pip install flask-cors
然后将 CORS 应用于 Flask:
from flask import Flask, make_response, request, render_template, jsonify
from flask_cors import CORS
app = Flask(__name__)
CORS(app=app)
@app.route("/", methods=["GET"])
def index():
return render_template("index.html")
@app.route("/get-cookie/", methods=["GET"])
def get_cookie():
response = make_response("Here, take some cookie!")
response.set_cookie(key="id", value="3db4adj3d")
return response
@app.route("/api/cities/", methods=["GET"])
def cities():
if request.cookies["id"] == "3db4adj3d":
cities = [{"name": "Rome", "id": 1}, {"name": "Siena", "id": 2}]
return jsonify(cities)
return jsonify(msg="Ops!")
现在尝试在浏览器控制台打开的情况下再次点击该按钮。在控制台中你应该看到:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:5000/api/cities/. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing)
尽管我们遇到了同样的错误,但这次的罪魁祸首在于第二条路线。
请求中没有附加名为“id”的 cookie,因此 Flask 崩溃并且没有Access-Control-Allow-Origin
设置。
您可以通过查看“网络”选项卡中的请求来确认这一点。没有Cookie
发送任何此类请求:
为了在跨不同来源的 Fetch 请求中包含 cookie,我们必须提供 credentials
标志(默认情况下是同源的)。
如果没有此标志,Fetch 将直接忽略 cookies。修复我们的示例:
const button = document.getElementsByTagName("button")[0];
button.addEventListener("click", function() {
getACookie().then(() => getData());
});
function getACookie() {
return fetch("http://localhost:5000/get-cookie/", {
credentials: "include"
}).then(response => {
// make sure to check response.ok in the real world!
return Promise.resolve("All good, fetch the data");
});
}
function getData() {
fetch("http://localhost:5000/api/cities/", {
credentials: "include"
})
.then(response => {
// make sure to check response.ok in the real world!
return response.json();
})
.then(json => console.log(json));
}
credentials: "include"
必须出现在第一个 Fetch 请求中,以将 cookie 保存在浏览器的 Cookie 存储中:
fetch("http://localhost:5000/get-cookie/", {
credentials: "include"
})
它还必须出现在第二个请求中,以允许将 cookie 传回后端:
fetch("http://localhost:5000/api/cities/", {
credentials: "include"
})
再试一次,你会发现我们需要修复后端的另一个错误:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:5000/get-cookie/. (Reason: expected ‘true’ in CORS header ‘Access-Control-Allow-Credentials’).
为了允许在 CORS 请求中传输 Cookie,后端Access-Control-Allow-Credentials
也需要公开标头。解决方法很简单:
CORS(app=app, supports_credentials=True)
现在您应该在浏览器的控制台中看到预期的城市数组。
要点:为了使 cookie 通过 AJAX 请求在不同来源之间传输,请提供:
credentials: "include"
在前端进行 FetchAccess-Control-Allow-Credentials
以及Access-Control-Allow-Origin
后端。
Cookies 可以通过 AJAX 请求传播,但它们必须遵守我们之前描述的域规则。
资源:
一个具体的例子
我们之前的示例使用 localhost 来使事情保持简单并可在本地机器上复制。
为了想象现实世界中通过 AJAX 请求交换 cookie,您可以考虑以下场景:
- 用户访问https://www.a-example.dev
- 她点击了一个按钮或做了一些操作,触发了对https://api.b-example.dev 的Fetch 请求
- https://api.b-example.dev设置了 cookie
Domain=api.b-example.dev
- 在后续对https://api.b-example.dev的 Fetch 请求中,cookie 会被发回
Cookies 可以是一种秘密:Secure 属性
但毕竟不是那么秘密。
Cookie 的属性Secure
确保 Cookie永远不会通过 HTTP 被接受,也就是说,除非连接通过 HTTPS 进行,否则浏览器会拒绝安全 Cookie。
要将 Cookie 标记为Secure
传递 Cookie 中的属性:
Set-Cookie: "id=3db4adj3d; Secure"
在 Flask 中:
response.set_cookie(key="id", value="3db4adj3d", secure=True)
如果您想在实时环境中尝试,请在控制台上运行以下命令,并注意此处的 curl 如何不通过 HTTP 保存 cookie:
curl -I http://serene-bastion-01422.herokuapp.com/get-secure-cookie/ --cookie-jar -
注意:此方法仅适用于实现了 rfc6265bis 的 curl 7.64.0 及以上版本。旧版本的 curl 已实现 RCF6265。请参阅
相反,通过 HTTPS,cookie 会出现在 cookie 罐中:
curl -I https://serene-bastion-01422.herokuapp.com/get-secure-cookie/ --cookie-jar -
这是罐子:
serene-bastion-01422.herokuapp.com FALSE / TRUE 0 id 3db4adj3d
要在浏览器中尝试 cookie,请访问上述两个版本的 url,并在开发人员工具中检查 Cookie 存储。
不要被欺骗:浏览器通过HTTPSSecure
接受 cookie ,但一旦 cookie 进入浏览器,就没有任何保护。
因此Secure
,cookie 与任何 cookie 一样,不用于传输敏感数据,即使其名称暗示了相反的意思。
不要碰我的 cookie:HttpOnly 属性
HttpOnly
Cookie 的属性确保 JavaScript 代码无法访问该 Cookie 。这是防范 XSS 攻击最重要的措施。
但是,它会在每个后续 HTTP 请求中发送,并考虑由Domain
和 强制执行的任何权限Path
。
要将 Cookie 标记为HttpOnly
传递 Cookie 中的属性:
Set-Cookie: "id=3db4adj3d; HttpOnly"
在 Flask 中:
response.set_cookie(key="id", value="3db4adj3d", httponly=True)
标记为无法从 JavaScript 访问的 cookie HttpOnly
:如果在控制台中检查,document.cookie
则返回一个空字符串。
但是,当设置为时, Fetch 可以获取并发回 HttpOnly
cookie ,同样,对于 和 强制执行的任何权限也是如此:credentials
include
Domain
Path
fetch(/* url */, {
credentials: "include"
})
何时使用HttpOnly
?只要能用就用。Cookie 应该始终是HttpOnly
,除非有特殊要求将它们暴露给运行时 JavaScript。
资源:
可怕的 SameSite 属性
第一方和第三方 Cookie
考虑通过访问 https://serene-bastion-01422.herokuapp.com/get-cookie/ 获取的 cookie :
Set-Cookie: simplecookiename=c00l-c00k13; Path=/
我们将这类 Cookie 称为“第一方”。也就是说,我在浏览器中访问该 URL,如果我访问相同的 URL,或者该网站的其他路径(假设Path
是/),浏览器就会将 Cookie 发送回网站。这是正常的 Cookie 行为。
现在考虑另一个网页https://serene-bastion-01422.herokuapp.com/get-frog/。该网页也设置了一个 cookie,并且从托管在https://www.valentinog.com/cookie-frog.jpg的远程资源加载了一张图片。
这个远程资源又会自行设置一个 Cookie。你可以在这张图中看到实际情况:
注意:如果您使用的是 Chrome 85,则不会看到此 Cookie。从此版本开始,Chrome 会拒绝此 Cookie。
我们将这类 Cookie 称为第三方 Cookie。第三方 Cookie 的另一个示例:
- 用户访问https://www.a-example.dev
- 她点击了一个按钮或做了一些操作,触发了对https://api.b-example.dev 的Fetch 请求
- https://api.b-example.dev设置了 cookie
Domain=api.b-example.dev
- 现在https://www.a-example.dev上的页面持有来自https://api.b-example.dev的第三方cookie
使用 SameSite
在撰写本文时,第三方 Cookie 会导致Chrome控制台中弹出警告:
与http://www.valentinog.com/上的跨站资源关联的 Cookie未设置 SameSite 属性。Chrome 的未来版本将仅传递跨站请求中设置了 SameSite=None 且 Secure 的 Cookie。
浏览器试图表明第三方 Cookie必须具有新SameSite
属性。但为什么呢?
该SameSite
属性是为了提高cookie安全性而新增的功能,以:防止跨站请求伪造攻击,避免隐私泄露。
SameSite
可以分配以下三个值之一:
- 严格的
- 松弛
- 没有任何
如果我们是提供可嵌入小部件(iframe)的服务,或者我们需要在远程网站中放置 cookie(出于正当理由而不是为了进行野外追踪),则这些 cookie 必须标记为SameSite=None
、 和Secure
:
Set-Cookie: frogcookie=fr0g-c00k13; SameSite=None; Secure
如果不这样做,浏览器将拒绝第三方 Cookie。以下是浏览器在不久的将来会采取的措施:
与http://www.valentinog.com/上的跨站资源关联的 Cookie未设置 SameSite 属性。该 Cookie 已被阻止,因为 Chrome 现仅会在跨站请求中传递设置了 SameSite=None 和 Secure 属性的 Cookie。
换句话说,SameSite=None; Secure
将使第三方 cookie 像现在一样工作,唯一的区别是它们必须仅通过 HTTPS 传输。
如果域名和路径匹配,则以这种方式配置的 Cookie 会随每个请求一起发送。这是正常行为。
值得注意的是,SameSite
这不仅仅涉及第三方 cookie。
默认情况下,如果缺少该属性,浏览器将强制执行 SameSite=Lax
所有 Cookie(包括第一方和第三方 Cookie)。以下是 Firefox Nightly 对第一方 Cookie 的测试结果:
Cookie“get_frog_simplecookiename”的“sameSite”策略设置为“lax”,因为它缺少“sameSite”属性,而“sameSite=lax”是该属性的默认值。
使用安全的 HTTP 方法(即 GET、HEAD、OPTIONS 和 TRACE)SameSite=Lax
会返回 cookie。POST请求则不会携带 cookie。
相反,第三方 cookieSameSite=Strict
将被浏览器完全拒绝。
回顾一下,以下是浏览器对于不同值的行为 SameSite
:
价值 | 传入 Cookie | 外发 Cookie |
---|---|---|
严格的 | 拒绝 | - |
松弛 | 接受 | 使用安全的 HTTP 方法发送 |
无+安全 | 接受 | 发送 |
要详细SameSite
了解此属性的所有用例,请阅读以下精彩资源:
Cookie 和身份验证
身份验证是 Web 开发中最具挑战性的任务之一。围绕这个话题似乎存在很多困惑,因为基于 JWT 的 token 身份验证似乎正在取代基于 session 的身份验证等“老旧”、稳定的模式。
让我们看看 cookies 在这里扮演什么角色。
基于会话的身份验证
身份验证是 Cookie 最常见的用例之一。
当您访问要求身份验证的网站时,在提交凭证(例如通过表单)时,后端会在后台Set-Cookie
向前端发送一个标题。
典型的会话 cookie 如下所示:
Set-Cookie: sessionid=sty1z3kz11mpqxjv648mqwlx4ginpt6c; expires=Tue, 09 Jun 2020 15:46:52 GMT; HttpOnly; Max-Age=1209600; Path=/; SameSite=Lax
在此Set-Cookie
标头中,服务器可能包含名为session、session id 或类似的cookie 。
这是浏览器唯一能清晰识别的标识符。每当经过身份验证的用户向后端请求新页面时,浏览器都会发回会话 cookie。
此时,后端将会话 ID 与存储在后台存储器中的会话配对,以正确识别用户。
基于会话的身份验证被称为有状态的,因为后端必须跟踪每个用户的会话。这些会话的存储可能是:
- 数据库
- 像 Redis 这样的键/值存储
- 文件系统
在这三种会话存储中,Redis 等应该比数据库或文件系统更受欢迎。
请注意,基于会话的身份验证与浏览器的会话存储无关。
之所以称为基于会话,是因为用户识别的相关数据存储在后端的会话存储中,这与浏览器的会话存储不同。
何时使用基于会话的身份验证?
尽可能使用它。基于会话的身份验证是最简单、最安全、最直接的网站身份验证形式之一。它在所有最流行的 Web 框架(例如 Django)上默认可用。
但是,其状态特性也是其主要缺点,尤其是在网站由负载均衡器提供服务时。在这种情况下,诸如粘性会话或将会话存储在集中式 Redis 存储上之类的技术可能会有所帮助。
关于 JWT 的说明
JWT 是 JSON Web Tokens 的缩写,是一种身份验证机制,近年来越来越受欢迎。
JWT 非常适合单页应用和移动应用,但它也带来了一系列新的挑战。前端应用进行 API 身份验证的典型流程如下:
- 前端向后端发送凭证
- 后端检查凭证并发回令牌
- 前端在每个后续请求上发送令牌
这种方法带来的主要问题是:我应该在前端的哪里存储这个令牌以保持用户登录?
对于编写 JavaScript 的人来说,最自然的事情就是将令牌保存在 中localStorage
。出于很多原因,这都是不好的。
localStorage
很容易通过 JavaScript 代码访问,并且很容易成为 XSS 攻击的目标。
为了解决这个问题,大多数开发人员都选择将 JWT 令牌保存在 cookie 中,认为这样HttpOnly
可以Secure
保护 cookie,至少可以防止 XSS 攻击。
SameSite
设置为 的新属性SameSite=Strict
也能保护你的“cookified” JWT 免受 CSRF 攻击。但是,由于跨域请求时不会发送 Cookie,因此它也完全使 JWT 的用例无效!SameSite=Strict
那么怎么样?此模式允许使用安全的 HTTP 方法SameSite=Lax
(即 GET、HEAD、OPTIONS 和 TRACE)发送 Cookie。POST请求无论如何都不会传输 Cookie。
实际上,将 JWT 令牌存储在 cookie 中或中localStorage
都是坏主意。
如果您确实想使用 JWT 而不是坚持基于会话的身份验证,并扩展会话存储,则可能需要使用带有刷新令牌的 JWT来保持用户登录状态。
资源:
总结
HTTP cookies 自 1994 年就已经存在。它们无处不在。
Cookie 是简单的文本字符串,但可以通过Domain
和 针对权限进行微调,Path
使用 仅通过 HTTPS 传输Secure
,使用 隐藏在 JavaScript 中HttpOnly
。
Cookie 可能用于个性化用户体验、用户身份验证或跟踪等可疑目的。
但是,对于所有预期用途,cookie 都可能使用户面临攻击和漏洞。
浏览器供应商和互联网工程任务组年复一年地致力于提高 cookie 的安全性,最近的一步是SameSite
。
那么,什么才是安全的 Cookie?没有这种东西。我们可以认为相对安全的 Cookie 是:
- 仅通过 HTTPS 传输,也就是说
Secure
- 只要有
HttpOnly
可能 - 具有正确的
SameSite
配置 - 不携带敏感数据
感谢阅读!
更多资源
特色图片中的图标由freepik提供。
文章来源:https://dev.to/valentinogagliardi/a-practical-complete-tutorial-on-http-cookies-1ofd