关于 HTTP cookie 的实用完整教程

2025-05-28

关于 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
Enter fullscreen mode Exit fullscreen mode

在项目文件夹中创建一个名为的新文件flask_app.py,并使用我的示例进行本地实验。

谁创建了 cookies?

首先,Cookie 从何而来?谁创建了 Cookie?

虽然可以使用在浏览器中创建 cookie document.cookie,但大多数情况下,后端有责任在将响应发送到客户端之前在响应中设置 cookie

这里所说的后端是指可以通过以下方式创建 cookie:

  • 后端的实际应用程序代码(Python、JavaScript、PHP、Java)
  • 响应请求的网络服务器(Nginx、Apache)

为此,后端在响应中设置一个 HTTP 标头,该标头Set-Cookie以由键/值对组成的相应字符串命名,并附加可选属性:

Set-Cookie: myfirstcookie=somecookievalue
Enter fullscreen mode Exit fullscreen mode

何时何地创建这些 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
Enter fullscreen mode Exit fullscreen mode

然后运行该应用程序:

FLASK_ENV=development FLASK_APP=flask_app.py flask run
Enter fullscreen mode Exit fullscreen mode

当此应用程序运行时,用户访问http://127.0.0.1:5000/index/后端会设置一个以键/值对命名响应头。Set-Cookie

(127.0.0.1:5000是开发中Flask应用程序的默认监听地址/端口)。

Set-Cookie头是了解如何创建 Cookie 的关键:

response.headers["Set-Cookie"] = "myfirstcookie=somecookievalue"
Enter fullscreen mode Exit fullscreen mode

在右侧你可以看到实际的 cookie "myfirstcookie=somecookievalue"

大多数框架都有自己的实用函数,用于以编程方式设置 cookie,例如Flask 的 set_cookie()

在底层,他们只是在响应中设置了一个标题Set-Cookie

如何查看cookie?

再次回顾前面使用 Flask 的示例。访问http://127.0.0.1:5000/index/document.cookie后,后端会在浏览器中设置一个 Cookie。要查看此 Cookie,您可以从浏览器控制台调用:

文档.cookie

或者,您可以检查开发者工具中的“存储”选项卡。点击“Cookies”,您应该在那里看到 cookie:

Cookie 存储

在命令行上,您还可以使用curl来查看后端设置的 cookie:

curl -I http://127.0.0.1:5000/index/
Enter fullscreen mode Exit fullscreen mode

要将 Cookie 保存到文件以供日后使用:

curl -I http://127.0.0.1:5000/index/ --cookie-jar mycookies
Enter fullscreen mode Exit fullscreen mode

要在标准输出上显示 cookie:

curl -I http://127.0.0.1:5000/index/ --cookie-jar -
Enter fullscreen mode Exit fullscreen mode

请注意,不带该HttpOnly属性的 Cookie 可以通过浏览器中的 JavaScript 访问document.cookie。另一方面,标记为 的 Cookie 则HttpOnly无法通过 JavaScript 访问。

要将 Cookie 标记为HttpOnly传递 Cookie 中的属性:

Set-Cookie: myfirstcookie=somecookievalue; HttpOnly
Enter fullscreen mode Exit fullscreen mode

现在该 cookie 仍会出现在 Cookie 存储选项卡中,但document.cookie会返回一个空字符串。

现在开始,为了方便起见,我将使用 Flask response.set_cookie() 在后端创建 cookie

为了在本指南中检查 Cookie,我们将使用以下方法:

  • 卷曲
  • Firefox 开发者工具
  • Chrome 开发者工具

我得到了一块饼干,现在怎么办?

你的浏览器获取了一个 Cookie。接下来怎么办?一旦获取到 Cookie,浏览器就可以将其发送回后端

这可能有许多应用:用户跟踪、个性化,以及最重要的身份验证

例如,一旦你登录网站,后端就可以给你一个 cookie:

Set-Cookie: userid=sup3r4n0m-us3r-1d3nt1f13r
Enter fullscreen mode Exit fullscreen mode

为了在每个后续请求中正确识别您,后端会检查请求中来自浏览器的 cookie

为了发送 cookie,浏览器会Cookie在请求中附加一个标头:

Cookie: userid=sup3r4n0m-us3r-1d3nt1f13r
Enter fullscreen mode Exit fullscreen mode

浏览器如何、何时以及为何发回 Cookie是下一节的主题。

Cookies 会过期:Max-Age 和 expires

默认情况下,Cookie 会在用户关闭会话(即关闭浏览器)时过期。为了持久化 Cookie,我们可以传递expiresMax-Age属性:

Set-Cookie: myfirstcookie=somecookievalue; expires=Tue, 09 Jun 2020 15:46:52 GMT; Max-Age=1209600
Enter fullscreen mode Exit fullscreen mode

当存在机器人属性时,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!"
Enter fullscreen mode Exit fullscreen mode

要运行该应用程序:

FLASK_ENV=development FLASK_APP=flask_app.py flask run
Enter fullscreen mode Exit fullscreen mode

在另一个终端中,如果我们与根路由建立连接,我们可以看到 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
Enter fullscreen mode Exit fullscreen mode

注意 cookies 有一个Path属性:

Set-Cookie: id=3db4adj3d; Path=/about/
Enter fullscreen mode Exit fullscreen mode

现在让我们通过发送第一次访问时保存的 cookie 来访问 /about/ 路由:

curl -I http://127.0.0.1:5000/about/ --cookie cookies
Enter fullscreen mode Exit fullscreen mode

在 Flask 应用程序运行的终端中,您应该看到:

ImmutableMultiDict([('id', '3db4adj3d')])
127.0.0.1 - - [27/May/2020 11:27:55] "HEAD /about/ HTTP/1.1" 200 -
Enter fullscreen mode Exit fullscreen mode

正如预期的那样,cookie 返回到了后端。现在尝试访问 /contact/ 路由:

curl -I http://127.0.0.1:5000/contact/ --cookie cookies
Enter fullscreen mode Exit fullscreen mode

这次在 Flask 应用程序运行的终端中您应该看到:

ImmutableMultiDict([])
127.0.0.1 - - [27/May/2020 11:29:00] "HEAD /contact/ HTTP/1.1" 200 -
Enter fullscreen mode Exit fullscreen mode

这是什么意思?Cookie 的作用域受路径限制。具有指定属性的 Cookie Path 不能发送到另一个不相关的路径,即使这两个路径位于同一个域中

这是cookie的第一层权限。

Path在创建 cookie 期间省略时,浏览器默认为/

Cookie 的范围由域决定:Domain 属性

DomainCookie 属性的值控制浏览器是否接受它以及Cookie返回到哪里

让我们看一些例子。

注意:以下 URL 是在免费的 Heroku 实例上。请稍等片刻,等待其启动。打开链接之前,请先打开浏览器的控制台,以便在网络选项卡中查看结果。

主机不匹配(错误的主机)

考虑以下由https://serene-bastion-01422.herokuapp.com/get-wrong-domain-cookie/ 设置的 cookie:

Set-Cookie: coookiename=wr0ng-d0m41n-c00k13; Domain=api.valentinog.com
Enter fullscreen mode Exit fullscreen mode

这里的 cookie 源自serene-bastion-01422.herokuapp.com,但Domain属性有api.valentinog.com

浏览器没有其他选择来拒绝此 Cookie。例如,Chrome 会发出警告(Firefox 不会):

浏览器在 cookie 中阻止了错误的域

不匹配的主机(子域名)

考虑以下由https://serene-bastion-01422.herokuapp.com/get-wrong-subdomain-cookie/ 设置的 cookie:

Set-Cookie: coookiename=wr0ng-subd0m41n-c00k13; Domain=secure-brushlands-44802.herokuapp.com
Enter fullscreen mode Exit fullscreen mode

这里的 cookie 源自serene-bastion-01422.herokuapp.com,但Domain属性是secure-brushlands-44802.herokuapp.com

它们位于同一个域名,但子域​​名不同。同样,浏览器也会拒绝此 Cookie:

浏览器在 cookie 中阻止了错误的域

匹配主机(整个域)

现在考虑通过访问 https://www.valentinog.com/get-domain-cookie.html 设置的以下 cookie

set-cookie: cookiename=d0m41n-c00k13; Domain=valentinog.com
Enter fullscreen mode Exit fullscreen mode

此 cookie 是在 Web 服务器级别使用Nginx add_header设置的:

add_header Set-Cookie "cookiename=d0m41n-c00k13; Domain=valentinog.com";
Enter fullscreen mode Exit fullscreen mode

这里我使用 Nginx 来演示设置 Cookie 的各种方法。Cookie 是由 Web 服务器设置还是由应用程序代码设置,对浏览器来说并不重要

重要的是 cookie 来自哪个域。

这里浏览器会很乐意接受该 cookie,因为中的主机Domain 包含了该 cookie 所来自的主机

换句话说,valentinog.com 包含子域名www.valentinog.com

此外,任何针对 valentinog.com 的新请求以及任何针对 valentinog.com 子域的请求都会返回此 cookie

以下是对 www 子域的附加了 cookie 的请求:

发回匹配域的 Cookie

以下是对另一个子域的请求,其中自动附加了 cookie:

Cookie 子域发回匹配的域

Cookies 和公共后缀列表

现在考虑由 https://serene-bastion-01422.herokuapp.com/get-domain-cookie/ 设置的以下 cookie

Set-Cookie: coookiename=d0m41n-c00k13; Domain=herokuapp.com
Enter fullscreen mode Exit fullscreen mode

这里的 Cookie 来自serene-bastion-01422.herokuapp.com,属性Domainherokuapp.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
Enter fullscreen mode Exit fullscreen mode

Domain在 cookie 创建过程中省略时,浏览器会默认使用地址栏中的原始主机,在这种情况下我的代码会执行以下操作:

response.set_cookie(key="coookiename", value="subd0m41n-c00k13")
Enter fullscreen mode Exit fullscreen mode

当 Cookie 进入浏览器的 Cookie 存储时,我们会看到Domain应用的内容:

默认域属性

所以我们从 serene-bastion-01422.herokuapp.com 获取了此 cookie。现在应该将此 cookie 发送到哪里

如果您访问https://serene-bastion-01422.herokuapp.com/,cookie会随请求一起出现:

Cookie 子域已发送

但是,如果您访问 herokuapp.com,cookie 根本不会离开浏览器

未将子域中的 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
Enter fullscreen mode Exit fullscreen mode

这是模板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>
Enter fullscreen mode Exit fullscreen mode

以下是 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));
}
Enter fullscreen mode Exit fullscreen mode

访问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!")
Enter fullscreen mode Exit fullscreen mode

另外,让我们调整一下 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));
}
Enter fullscreen mode Exit fullscreen mode

访问http://127.0.0.1:5000/时,我们会看到一个按钮。点击按钮后,我们会向 /get-cookie/ 发出一个 Fetch 请求,以获取 cookie。获取到 cookie 后,我们会立即向 /api/cities/ 发出另一个 Fetch 请求。

在浏览器的控制台中,你应该看到一个城市数组。此外,在开发者工具的“网络”选项卡中,你应该看到一个名为 的标头Cookie,它通过 AJAX 请求传输到后端:

获取 API cookie 标头

只要前端与后端处于相同的上下文中,前端和后端之间的 cookie 交换就可以正常工作我们说它们位于同一来源。

这是因为默认情况下,Fetch仅当请求到达与请求触发相同的来源时才会发送凭据,即 cookie 。

这里,JavaScript 由http://127.0.0.1:5000/上的 Flask 模板提供

让我们看看不同来源会发生什么。

Cookie 并不总是能够通过 AJAX 请求传输

考虑后端独立运行的不同情况,因此您运行此 Flask 应用程序:

FLASK_ENV=development FLASK_APP=flask_app.py flask run
Enter fullscreen mode Exit fullscreen mode

现在在 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>
Enter fullscreen mode Exit fullscreen mode

在同一文件夹中创建一个名为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));
}
Enter fullscreen mode Exit fullscreen mode

在同一文件夹中,从终端运行:

npx serve
Enter fullscreen mode Exit fullscreen mode

此命令会提供一个本地地址/端口供您连接,例如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)
Enter fullscreen mode Exit fullscreen mode

现在,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
Enter fullscreen mode Exit fullscreen mode

然后将 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!")
Enter fullscreen mode Exit fullscreen mode

现在尝试在浏览器控制台打开的情况下再次点击该按钮。在控制台中你应该看到:

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)
Enter fullscreen mode Exit fullscreen mode

尽管我们遇到了同样的错误,但这次的罪魁祸首在于第二条路线。

请求中没有附加名为“id”的 cookie,因此 Flask 崩溃并且没有Access-Control-Allow-Origin设置。

您可以通过查看“网络”选项卡中的请求来确认这一点。没有Cookie发送任何此类请求:

未获取附加的 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));
}
Enter fullscreen mode Exit fullscreen mode

credentials: "include"必须出现在第一个 Fetch 请求中,以将 cookie 保存在浏览器的 Cookie 存储中:

fetch("http://localhost:5000/get-cookie/", {
    credentials: "include"
  })
Enter fullscreen mode Exit fullscreen mode

它还必须出现在第二个请求中,以允许将 cookie 传回后端:

  fetch("http://localhost:5000/api/cities/", {
    credentials: "include"
  })
Enter fullscreen mode Exit fullscreen mode

再试一次,你会发现我们需要修复后端的另一个错误:

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’).
Enter fullscreen mode Exit fullscreen mode

为了允许在 CORS 请求中传输 Cookie,后端Access-Control-Allow-Credentials也需要公开标头。解决方法很简单:

CORS(app=app, supports_credentials=True)
Enter fullscreen mode Exit fullscreen mode

现在您应该在浏览器的控制台中看到预期的城市数组。

要点:为了使 cookie 通过 AJAX 请求在不同来源之间传输,请提供:

  • credentials: "include"在前端进行 Fetch
  • Access-Control-Allow-Credentials以及Access-Control-Allow-Origin后端。

Cookies 可以通过 AJAX 请求传播,但它们必须遵守我们之前描述的域规则

资源:

一个具体的例子

我们之前的示例使用 localhost 来使事情保持简单并可在本地机器上复制。

为了想象现实世界中通过 AJAX 请求交换 cookie,您可以考虑以下场景:

  1. 用户访问https://www.a-example.dev
  2. 她点击了一个按钮或做了一些操作,触发了对https://api.b-example.dev 的Fetch 请求
  3. https://api.b-example.dev设置了 cookieDomain=api.b-example.dev
  4. 在后续对https://api.b-example.dev的 Fetch 请求中,cookie 会被发回

Cookies 可以是一种秘密:Secure 属性

但毕竟不是那么秘密。

Cookie 的属性Secure确保 Cookie永远不会通过 HTTP 被接受,也就是说,除非连接通过 HTTPS 进行,否则浏览器会拒绝安全 Cookie

要将 Cookie 标记为Secure传递 Cookie 中的属性:

Set-Cookie: "id=3db4adj3d; Secure"
Enter fullscreen mode Exit fullscreen mode

在 Flask 中:

response.set_cookie(key="id", value="3db4adj3d", secure=True)
Enter fullscreen mode Exit fullscreen mode

如果您想在实时环境中尝试,请在控制台上运行以下命令,并注意此处的 curl 如何不通过 HTTP 保存 cookie

curl -I http://serene-bastion-01422.herokuapp.com/get-secure-cookie/ --cookie-jar -
Enter fullscreen mode Exit fullscreen mode

注意:此方法仅适用于实现了 rfc6265bis 的 curl 7.64.0 及以上版本。旧版本的 curl 已实现 RCF6265。请参阅

相反,通过 HTTPS,cookie 会出现在 cookie 罐中:

curl -I https://serene-bastion-01422.herokuapp.com/get-secure-cookie/ --cookie-jar -
Enter fullscreen mode Exit fullscreen mode

这是罐子:

serene-bastion-01422.herokuapp.com      FALSE   /       TRUE    0       id      3db4adj3d
Enter fullscreen mode Exit fullscreen mode

要在浏览器中尝试 cookie,请访问上述两个版本的 url,并在开发人员工具中检查 Cookie 存储。

不要被欺骗:浏览器通过HTTPSSecure接受 cookie ,但一旦 cookie 进入浏览器,就没有任何保护。

因此Secure ,cookie 与任何 cookie 一样,不用于传输敏感数据,即使其名称暗示了相反的意思。

不要碰我的 cookie:HttpOnly 属性

HttpOnlyCookie 的属性确保 JavaScript 代码无法访问该 Cookie 这是防范 XSS 攻击最重要的措施。

但是,它会在每个后续 HTTP 请求中发送,并考虑由Domain和 强制执行的任何权限Path

要将 Cookie 标记为HttpOnly传递 Cookie 中的属性:

Set-Cookie: "id=3db4adj3d; HttpOnly"
Enter fullscreen mode Exit fullscreen mode

在 Flask 中:

response.set_cookie(key="id", value="3db4adj3d", httponly=True)
Enter fullscreen mode Exit fullscreen mode

标记为无法从 JavaScript 访问的 cookie HttpOnly:如果在控制台中检查,document.cookie则返回一个空字符串。

但是,当设置为时, Fetch 可以获取并发回 HttpOnlycookie ,同样,对于 和 强制执行的任何权限也是如此credentialsincludeDomainPath

fetch(/* url */, {
  credentials: "include"
})
Enter fullscreen mode Exit fullscreen mode

何时使用HttpOnly只要能用就用。Cookie 应该始终是HttpOnly,除非有特殊要求将它们暴露给运行时 JavaScript。

资源:

可怕的 SameSite 属性

第一方和第三方 Cookie

考虑通过访问 https://serene-bastion-01422.herokuapp.com/get-cookie/ 获取的 cookie

Set-Cookie: simplecookiename=c00l-c00k13; Path=/
Enter fullscreen mode Exit fullscreen mode

我们将这类 Cookie 称为“第一方”。也就是说,我在浏览器中访问该 URL,如果我访问相同的 URL,或者该网站的其他路径(假设Path/),浏览器就会将 Cookie 发送回网站。这是正常的 Cookie 行为。

现在考虑另一个网页https://serene-bastion-01422.herokuapp.com/get-frog/。该网页也设置了一个 cookie,并且从托管在https://www.valentinog.com/cookie-frog.jpg的远程资源加载了一张图片

这个远程资源又会自行设置一个 Cookie。你可以在这张图中看到实际情况:

第三方 Cookie

注意:如果您使用的是 Chrome 85,则不会看到此 Cookie。从此版本开始,Chrome 会拒绝此 Cookie。

我们将这类 Cookie 称为第三方 Cookie。第三方 Cookie 的另一个示例:

  1. 用户访问https://www.a-example.dev
  2. 她点击了一个按钮或做了一些操作,触发了对https://api.b-example.dev 的Fetch 请求
  3. https://api.b-example.dev设置了 cookieDomain=api.b-example.dev
  4. 现在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
Enter fullscreen mode Exit fullscreen mode

如果不这样做,浏览器将拒绝第三方 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
Enter fullscreen mode Exit fullscreen mode

在此Set-Cookie标头中,服务器可能包含名为session、session id 或类似的cookie 。

这是浏览器唯一能清晰识别的标识符。每当经过身份验证的用户向后端请求新页面时,浏览器都会发回会话 cookie

此时,后端将会话 ID 与存储在后台存储器中的会话配对,以正确识别用户。

基于会话的身份验证被称为有状态的,因为后端必须跟踪每个用户的会话。这些会话的存储可能是:

  • 数据库
  • 像 Redis 这样的键/值存储
  • 文件系统

在这三种会话存储中,Redis 等应该比数据库或文件系统更受欢迎。

请注意,基于会话的身份验证与浏览器的会话存储无关

之所以称为基于会话,是因为用户识别的相关数据存储在后端的会话存储中,这与浏览器的会话存储不同。

何时使用基于会话的身份验证?

尽可能使用它基于会话的身份验证是最简单、最安全、最直接的网站身份验证形式之一。它在所有最流行的 Web 框架(例如 Django)上默认可用。

但是,其状态特性也是其主要缺点,尤其是在网站由负载均衡器提供服务时。在这种情况下,诸如粘性会话将会话存储在集中式 Redis 存储上之类的技术可能会有所帮助。

关于 JWT 的说明

JWT 是 JSON Web Tokens 的缩写,是一种身份验证机制,近年来越来越受欢迎。

JWT 非常适合单页应用和移动应用,但它也带来了一系列新的挑战。前端应用进行 API 身份验证的典型流程如下:

  1. 前端向后端发送凭证
  2. 后端检查凭证并发回令牌
  3. 前端在每个后续请求上发送令牌

这种方法带来的主要问题是:我应该在前端的哪里存储这个令牌以保持用户登录?

对于编写 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
PREV
Jest 初学者教程:开始使用 Jest 进行 JavaScript 测试
NEXT
更快——无需鼠标即可进行 VSCode 导航