前端开发完整的 HTTP cookie 指南

目录
文章目录隐藏
  1. Web 开发中的 cookie 是什么?
  2. 后端配置
  3. 谁创建 cookies ?
  4. 如何查看 cookies ?
  5. 我有一个 cookie,现在怎么办?
  6. cookie 可以设置过期时间: Max-Age 和 expires
  7. cookie 的作用域是网站路径: path 属性
  8. cookie 的作用域是域名: domain 属性
  9. Cookies 和公共后缀列表
  10. Cookies 可以通过 AJAX 请求传递
  11. cookie 不能总是通过 AJAX 请求传递
  12. 处理 CORS
  13. Cookie 的 Secure 属性
  14. Cookie 的 HttpOnly 属性
  15. 可怕的 SameSite 属性
  16. Cookies 和 认证
  17. 总结

Web 开发中的 cookie 是什么?

cookie 是后端可以存储在用户浏览器中的小块数据。 Cookie 最常见用例包括用户跟踪,个性化以及身份验证。

Cookies 具有很多隐私问题,多年来一直受到严格的监管。

在本文中,主要侧重于技术方面:学习如何在前端和后端创建,使用 HTTP cookie。

后端配置

后端示例是Flask编写的。如果你想跟着学习,可以创建一个新的 Python 虚拟环境,移动到其中并安装 Flask。

mkdir cookies && cd $_

python3 -m venv venv
source venv/bin/activate

pip install Flask

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

谁创建 cookies ?

首先,cookies 从何而来? 谁创建 cookies ?

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

后端是指可以通过以下方式创建 Cookie:

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

后端可以在 HTTP 请求求中 Set-Cookie 属性来设置 cookie,它是由键/值对以及可选属性组成的相应字符串:

Set-Cookie: myfirstcookie=somecookievalue

什么时候需要创建 cookie? 这取决于需求。

cookie 是简单的字符串。在项目文件夹中创建一个名为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 的方法,比如 Flask 的set_cookie()

如何查看 cookies ?

访问http://127.0.0.1:5000/index/后,后端将在浏览器中设置 cookie。 要查看此 cookie,可以从浏览器的控制台调用document.cookie

如何查看 cookies ?

或者可以在开发人员工具中选中Storage选项卡。单击 cookie,会看到 cookie 具体的内容:

开发人员工具中选中 Storage 选项卡

在命令行上,还可以使用curl查看后端设置了哪些 cookie:

curl -I http://127.0.0.1:5000/index/

可以将 Cookie 保存到文件中以供以后使用:

curl -I http://127.0.0.1:5000/index/ --cookie-jar mycookies

在 stdout 上显示 cookie:

curl -I http://127.0.0.1:5000/index/ --cookie-jar -

请注意,没有HttpOnly属性的cookie,在浏览器中可以使用document.cookie上访问,如果设置了 HttpOnly 属性,document.cookie就读取不到。

Set-Cookie: myfirstcookie=somecookievalue; HttpOnly

现在,该 cookie 仍将出现在 Storage 选项卡中,但是 document.cookie返回的是一个空字符串。

从现在开始,为方便起见,使用 Flask 的 response.set_cookie() 在后端上创建 cookie。

我有一个 cookie,现在怎么办?

你的浏览器得到一个 cookie。现在怎么办呢?一旦有了 cookie,浏览器就可以将 cookie 发送回后端。

这有许多用途发如:用户跟踪、个性化,以及最重要的身份验证。

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

Set-Cookie: userid=sup3r4n0m-us3r-1d3nt1f13r

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

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

Cookie: userid=sup3r4n0m-us3r-1d3nt1f13r

cookie 可以设置过期时间: Max-Age 和 expires

默认情况下,cookie 在用户关闭会话时即关闭浏览器时过期。要持久化 cookie,我们可以通过expiresMax-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

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

请注意,此时 cookie 具有Path属性:

Set-Cookie: id=3db4adj3d; Path=/about/

/about/ 路由并保存 cookie:

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/ 路由:

url -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 的作用域是Path 。具有给定路径属性的 cookie 不能被发送到另一个不相关的路径,即使这两个路径位于同一域中。

这是 cookie 权限的第一层。

在 cookie 创建过程中省略Path时,浏览器默认为/

cookie 的作用域是域名: domain 属性

cookie 的 Domain 属性的值控制浏览器是否应该接受 cookie 以及 cookie 返回的位置。

让我们看一些例子。

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

查看 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 没有)

Chrome 会给出一个警告

主机不匹配(子域名)

查看 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:

浏览器也拒绝此 cookie

主机匹配(整个域)

查看 https://www.valentinog.com/get-domain-cookie.html设置的 cookie:

set-cookie: cookiename=d0m41n-c00k13; Domain=valentinog.com

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

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的新请求,cookie 都会携带着,以及任何对valentinog.com子域名的请求。

这是一个附加了 Cookie 的 www 子域请求:

附加了 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-bas-01422.herokuapp.comDomain 属性是herokuapp.com。浏览器在这里应该做什么

你可能认为serene-base-01422.herokuapp.com包含在herokuapp.com域中,因此浏览器应该接受 cookie。

相反,它拒绝 cookie,因为它来自公共后缀列表中包含的域。

Public Suffix List(公共后缀列表)。此列表列举了顶级域名和开放注册的域名。浏览器禁止此列表上的域名被子域名写入 Cookie。

主机匹配(子域)

查看 https://serene-bastion-01422.herokuapp.com/get-subdomain-cookie/:设置的 cookie:

Set-Cookie: coookiename=subd0m41n-c00k13

当域在 cookie 创建期间被省略时,浏览器会默认在地址栏中显示原始主机,在这种情况下,我的代码会这样做:

response.set_cookie(key="coookiename", value="subd0m41n-c00k13")

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

已应用 Domain

现在,我们有来自serene-bastion-01422.herokuapp.com 的 cookie, 那 cookie 现在应该送到哪里?

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

cookie 随请求一起出现

但是,如果访问 herokuapp. com,则 cookie 不会随请求一起出现:

cookie 不会随请求一起出现

概括地说,浏览器使用以下启发式规则来决定如何处理 cookies(这里的发送者主机指的是你访问的实际网址):

  • 如果“Domain”中的域或子域与访问的主机不匹配,则完全拒绝 Cookie
  • 如果 Domain 的值包含在公共后缀列表中,则拒绝 cookie
  • 如果Domain 中的域或子域与访问在主机匹配,则接受 Cookie

一旦浏览器接受了 cookie,并且即将发出请求,它就会说:

  • 如果请求主机与我在Domain中看到的值完全匹配,刚会回传 cookie
  • 如果请求主机是与我在“Domain”中看到的值完全匹配的子域,则将回传 cookie
  • 如果请求主机是sub.example.dev之类的子域,包含在example.dev之类的 Domain 中,则将回传 cookie
  • 如果请求主机是例如example.dev之类的主域,而 Domain 是sub.example.dev之类,则不会回传 cookie。

Domain 和 Path 属性一直是 cookie 权限的第二层。

Cookies 可以通过 AJAX 请求传递

Cookies 可以通过 AJAX 请求传播。 AJAX 请求是使用 JS (XMLHttpRequest 或 Fetch)进行的异步 HTTP 请求,用于获取数据并将其发送回后端。

考虑 Flask 的另一个示例,其中有一个模板,该模板又会加载 JS 文件:

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>

下面是 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/发出获取请求并获取 Cookie。 正如预期的那样,cookie 落在浏览器的 Cookie storage 中。

对 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!")

另外,调整一下 JS 代码,用于下请求刚新增的路由:

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/发出获取请求以获取 Cookie。 Cookie 出现后,我们就会对/api/cities/再次发出 Fetch 请求。

在浏览器的控制台中,可以看到请求回来 的数据。另外,在开发者工具的Network选项卡中,可以看到一个名为 Cookie 的头,这是通过 AJAX 请求传给后端。

只要前端与后端在同一上下文中,在前端和后端之间来回交换 cookie 就可以正常工作:我们说它们来自同一源。

这是因为默认情况下,Fetch 仅在请求到达触发请求的来源时才发送凭据,即 Cookie

cookie 不能总是通过 AJAX 请求传递

考虑另一种情况,在后端独立运行,可以这样启动应用程序:

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的 JS 文件:

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 的限制。

处理 CORS

CORS 是一个 W3C 标准,全称是“跨域资源共享”(Cross-origin resource sharing)。它允许浏览器向跨域的服务器,发出 XMLHttpRequest 请求,从而克服了 AJAX 只能同源使用的限制。

整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS 通信与普通的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨域,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感知。因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨域通信。

默认情况下,除非服务器设置了Access-Control-Allow-Origin的特定 HTTP 标头,否则浏览器将阻止 AJAX 对非相同来源的远程资源的请求。

要解决此第一个错误,我们需要为 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)

尽管我们犯了同样的错误,但这次的罪魁祸首是第二个路由。

你可以通过查看 “Network” 标签中的请求来确认,没有发送此类 Cookie:

没有发送此类 Cookie

为了在不同来源的 Fetch 请求中包含 cookie,我们必须提credentials 标志(默认情况下,它是相同来源)。

如果没有这个标志,Fetch 就会忽略 cookie,可以这样修复:

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 storage 中:

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” 用于前端的 fetch 请求中
  • Access-Control-Allow-Credentials 和 Access-Control-Allow-Origin 用于后端

cookie 可以通过 AJAX 请求传递,但是它们必须遵守我们前面描述的域规则。

Cookie 的 Secure 属性

Secure 属性是说如果一个 cookie 被设置了Secure=true,那么这个 cookie 只能用 https 协议发送给服务器,用 http 协议是不发送的。换句话说,cookie 是在https的情况下创建的,而且他的 Secure=true,那么之后你一直用 https 访问其他的页面(比如登录之后点击其他子页面),cookie 会被发送到服务器,你无需重新登录就可以跳转到其他页面。但是如果这是你把 url 改成 http 协议访问其他页面,你就需要重新登录了,因为这个 cookie 不能在 http 协议中发送。

可以这样设置 Secure 属性:

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 -

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

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

cookie jar 文件:

serene-bastion-01422.herokuapp.com      FALSE   /       TRUE    0

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

因为带有 Secure 的 Cookie 一般也不用于传输敏感数据.

Cookie 的 HttpOnly 属性

如果 cookie 中设置了HttpOnly属性,那么通过 js 脚本将无法读取到 cookie 信息,这样能有效的防止 XSS 攻击,窃取 cookie 内容,这样就增加了 cookie 的安全性,即便是这样,也不要将重要信息存入 cookie。

XSS 全称 Cross SiteScript,跨站脚本攻击,是 Web 程序中常见的漏洞,XSS 属于被动式且用于客户端的攻击方式,所以容易被忽略其危害性。其原理是攻击者向有 XSS 漏洞的网站中输入(传入)恶意的 HTML 代码,当其它用户浏览该网站时,这段 HTML 代码会自动执行,从而达到攻击的目的。如,盗取用户 Cookie、破坏页面结构、重定向到其它网站等。

如果有设置 HttpOnly 看起来是这样的:

Set-Cookie: "id=3db4adj3d; HttpOnly"

在 Flask 中:

response.set_cookie(key="id", value="3db4adj3d", httponly=True)

这样,cookie 设置了HttpOnly属性,那么通过 js 脚本将无法读取到 cookie 信息。如果在控制台中进行检查,则document.cookie将返回一个空字符串。

何时使用HttpOnly? cookie 应该始终是HttpOnly的,除非有特定的要求将它们暴露给运行时 JS。

可怕的 SameSite 属性

first-party cookie 和 third-party cookie

查看https://serene-bastion-01422.herokuapp.com/get-cookie/ 中所携带的 Cookie

Set-Cookie: simplecookiename=c00l-c00k13; Path=/

first-party是指你登录或使用的网站所发行的 cookie,而third-party cookie 常为一些广告网站,有侵犯隐私以及安全隐患。

我们将这类 Cookie 称为 first-party。 也就是说,我在浏览器中访问该 URL,并且如果我访问相同的 URL 或该站点的另一个路径(假设 Path 为/),则浏览器会将 cookie 发送回该网站。

现在考虑在https://serene-bastion-01422.herokuapp.com/get-frog/上的另一个网页。 该页面设置了一个 cookie,此外,它还从 https://www.valentinog.com/cookie-frog.jpg托管的远程资源中加载图像。

该远程资源又会自行设置一个 cookie:

远程资源又会自行设置一个 cookie

我们将这种 cookie 称为third-party(第三方) Cookie。

第三方 Cookie 除了用于 CSRF 攻击,还可以用于用户追踪。比如,码云笔记 在第三方网站插入一张看不见的图片。

![](mybj123.com)

浏览器加载上面代码时,就会向 码云笔记 发出带有 Cookie 的请求,从而 码云笔记 就会知道你是谁,访问了什么网站。

使用 SameSite 属性

Cookie 的 SameSite 属性用来限制third-party Cookie,从而减少安全风险。它可以设置三个值。

  • Strict
  • Lax
  • None

Strict最为严格,完全禁止第三方 Cookie,跨站点时,任何情况下都不会发送 Cookie。换言之,只有当前网页的 URL 与请求目标一致,才会带上 Cookie。

Set-Cookie: CookieName=CookieValue; SameSite=Strict;

这个规则过于严格,可能造成非常不好的用户体验。比如,当前网页有一个 GitHub 链接,用户点击跳转就不会带有 GitHub 的 Cookie,跳转过去总是未登陆状态。

Lax规则稍稍放宽,大多数情况也是不发送第三方 Cookie,但是导航到目标网址的 Get 请求除外。

Set-Cookie: CookieName=CookieValue; SameSite=Lax;

导航到目标网址的 GET 请求,只包括三种情况:链接,预加载请求,GET 表单。详见下表。

GET 表单

设置了StrictLax以后,基本就杜绝了 CSRF 攻击。当然,前提是用户浏览器支持 SameSite 属性。

Chrome 计划将Lax变为默认设置。这时,网站可以选择显式关闭SameSite属性,将其设为 None。不过,前提是必须同时设置Secure属性(Cookie 只能通过 HTTPS 协议发送),否则无效。

下面的设置无效:

Set-Cookie: widget_session=abc123; SameSite=None

下面的设置有效:

Set-Cookie: widget_session=abc123; SameSite=None; Secure

Cookies 和 认证

身份验证是 web 开发中最具挑战性的任务之一。关于这个主题似乎有很多困惑,因为JWT中的基于令牌的身份验证似乎要取代“旧的”、可靠的模式,如基于会话的身份验证。

来看看 cookie 在这里扮演什么角色。

基于会话的身份验证

身份验证是 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头中,服务器可以包括一个名为sessionsession id或类似的cookie

这是浏览器可以清楚看到的唯一标识符。 每当通过身份验证的用户向后端请求新页面时,浏览器就会发回会话cookie

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

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

在这三个会话存储中,Redis 之类应优先于数据库或文件系统。

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

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

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

只要能使用就使用它。基于会话的身份验证是一种最简单、安全、直接的网站身份验证形式。默认情况下,它可以在Django等所有流行的 web 框架上使用。

但是,它的状态特性也是它的主要缺点,特别是当网站是由负载均衡器提供服务时。在这种情况下,像粘贴会话,或者在集中的 Redis 存储上存储会话这样的技术会有所帮助。

关于 JWT 的说明

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

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

  • 前端将凭证发送到后端
  • 后端检查凭证并发回令牌
  • 前端在每个后续请求上带上该令牌

这种方法带来的主要问题是:为了使用户保持登录状态,我将该令牌存储在前端的哪个地方?

对于前端开发来说,最自然的事情是将令牌保存在localStorage中。 由于许多原因,这很糟糕。

localStorage很容易从 JS 代码访问,而且它很容易成为XSS攻击的目标。

为了解决此问题,大多数开发人员都将JWT令牌保存在cookie中,以为 HttpOnly 和Secure可以保护 cookie,至少可以免受 XSS 攻击。

将 SameSite 设置为 strict 就可以完全保护 JWT 免受 CSRF 攻击

设置为SameSite = Strict的新SameSite属性还将保护您的“熟化” JWT 免受 CSRF 攻击。 但是,由于SameSite = Strict不会在跨域请求上发送 cookie,因此,这也完全使 JWT 的用例无效。

SameSite=Lax呢? 此模式允许使用安全的 HTTP 方法(即 GET,HEAD,OPTIONS 和 TRACE)将 cookie 发送回去。 POST 请求不会以任何一种方式传输 cookie。

实际上,将JWT标记存储在cookielocalStorage中都不是好主意。

如果你确实要使用 JWT 而不是坚持使用基于会话的身份验证并扩展会话存储,则可能要使用带有刷新令牌的JWT来保持用户登录。

总结

自 1994 年以来,HTTP cookie 一直存在,它们无处不在。

Cookies 是简单的文本字符串,但可以通过DomainPath对其权限进行控制,具有 Secure 的 Cookie,只能通过 HTTP S 进行传输,而可以使用 HttpOnly从 JS 隐藏。

但是,对于所有预期的用途,cookie 都可能使用户暴露于攻击和漏洞之中。

浏览器的供应商和 Internet 工程任务组(Internet Engineering Task Force)年复一年地致力于提高 cookie 的安全性,最近的一步是SameSite

那么,什么才算是比较安全 cookie? ,如下几点:

  • 仅使用 HTTPS
  • 尽可能带有 HttpOnly 属性
  • 正确的 SameSite 配置
  • 不携带敏感数据

「点点赞赏,手留余香」

0

给作者打赏,鼓励TA抓紧创作!

微信微信 支付宝支付宝

还没有人赞赏,快来当第一个赞赏的人吧!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。
码云笔记 » 前端开发完整的 HTTP cookie 指南

发表回复