【必需知道】实用,完整的HTTP cookie指南

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

[[339019]]

Web 开发中的 cookie 是什么?

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

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

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

后端配置

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

  1. mkdir cookies && cd $_ 
  2.  
  3. python3 -m venv venv 
  4. source venv/bin/activate 
  5.  
  6. 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,它是由键/值对以及可选属性组成的相应字符串:

  1. Set-Cookie: myfirstcookie=somecookievalue 

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

cookie 是简单的字符串。在项目文件夹中创建一个名为flask_app.py的Python文件,并输入以下内容:

  1. from flask import Flask, make_response 
  2.  
  3. app = Flask(__name__) 
  4.  
  5.  
  6. @app.route("/index/", methods=["GET"]) 
  7. def index(): 
  8.     response = make_response("Here, take some cookie!"
  9.     response.headers["Set-Cookie"] = "myfirstcookie=somecookievalue" 
  10.     return response 

然后运行应用程序:

  1. 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的关键:

  1. response.headers["Set-Cookie"] = "myfirstcookie=somecookievalue" 

大多数框架都有自己设置 cookie 的方法,比如Flask的set_cookie()。

如何查看 cookies ?

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

 

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

 

 

 

 

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

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

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

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

在 stdout 上显示 cookie:

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

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

  1. Set-Cookie: myfirstcookie=somecookievalue; HttpOnly 

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

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

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

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

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

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

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

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

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

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

cookie 可以设置过期时间: Max-Age 和 expires默认情况下,cookie 在用户关闭会话时即关闭浏览器时过期。要持久化cookie,我们可以通过expires或Max-Age属性

  1. 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:

  1. from flask import Flask, make_response, request 
  2.  
  3. app = Flask(__name__) 
  4.  
  5.  
  6. @app.route("/", methods=["GET"]) 
  7. def index(): 
  8.     response = make_response("Here, take some cookie!"
  9.     response.set_cookie(key="id", value="3db4adj3d", path="/about/"
  10.     return response 
  11.  
  12.  
  13. @app.route("/about/", methods=["GET"]) 
  14. def about(): 
  15.     print(request.cookies) 
  16.     return "Hello world!" 
  17.  
  18.  
  19. @app.route("/contact/", methods=["GET"]) 
  20. def contact(): 
  21.     print(request.cookies) 
  22.     return "Hello world!" 

运行该应用程序:

  1. FLASK_ENV=development FLASK_APP=flask_app.py flask run 

在另一个终端中,如果我们与根路由建立连接,则可以在Set-Cookie中看到cookie:

  1. curl -I http://127.0.0.1:5000/ --cookie-jar cookies 
  2.  
  3. HTTP/1.0 200 OK 
  4. Content-Type: text/html; charset=utf-8 
  5. Content-Length: 23 
  6. Set-Cookie: id=3db4adj3d; Path=/about/ 
  7. Server: Werkzeug/1.0.1 Python/3.8.3 
  8. Date: Wed, 27 May 2020 09:21:37 GMT 

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

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

/about/ 路由并保存 cookit

  1. curl -I http://127.0.0.1:5000/about/ --cookie cookies 

在 Flask 应用程序的终端中运行如下命令,可以看到:

  1. ImmutableMultiDict([('id''3db4adj3d')]) 
  2. 127.0.0.1 - - [27/May/2020 11:27:55] "HEAD /about/ HTTP/1.1" 200 - 

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

  1. url -I http://127.0.0.1:5000/contact/ --cookie cookies 

在 Flask 应用程序的终端中运行如下命令,可以看到:

  1. ImmutableMultiDict([]) 
  2. 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:

  1. 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:

  1. 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:

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

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

  1. 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的子域的请求

 

Cookies 和公共后缀列表

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

  1. Set-Cookie: coookiename=d0m41n-c00k13; Domain=herokuapp.com 

这里的 cookie 来自serene-bas-01422.herokuapp.com,Domain 属性是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:

  1. Set-Cookie: coookiename=subd0m41n-c00k13 

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

  1. 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 不会随请求一起出现:

 

概括地说,浏览器使用以下启发式规则来决定如何处理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 文件:

  1. from flask import Flask, make_response, render_template 
  2.  
  3. app = Flask(__name__) 
  4.  
  5.  
  6. @app.route("/", methods=["GET"]) 
  7. def index(): 
  8.     return render_template("index.html"
  9.  
  10.  
  11. @app.route("/get-cookie/", methods=["GET"]) 
  12. def get_cookie(): 
  13.     response = make_response("Here, take some cookie!"
  14.     response.set_cookie(key="id", value="3db4adj3d"
  15.     return response 

以下是 templates/index.html 模板:

  1. <!DOCTYPE html> 
  2. <html lang="en"
  3. <head> 
  4.     <meta charset="UTF-8"
  5.     <title>Title</title> 
  6. </head> 
  7. <body> 
  8. <button>FETCH</button> 
  9. </body> 
  10. <script src="{{ url_for('static', filename='index.js') }}"></script> 
  11. </html> 

下面是 static/index.js 的内容:

  1. const button = document.getElementsByTagName("button")[0]; 
  2.  
  3. button.addEventListener("click"function() { 
  4.   getACookie(); 
  5. }); 
  6.  
  7. function getACookie() { 
  8.   fetch("/get-cookie/"
  9.     .then(response => { 
  10.       // make sure to check response.ok in the real world! 
  11.       return response.text(); 
  12.     }) 
  13.     .then(text => console.log(text)); 

当访问http://127.0.0.1:5000/时,我们会看到一个按钮。通过单击按钮,我们向/get-cookie/发出获取请求并获取Cookie。正如预期的那样,cookie 落在浏览器的 Cookie storage中。

对 Flask 应用程序进行一些更改,多加一个路由:

  1. from flask import Flask, make_response, request, render_template, jsonify 
  2.  
  3. app = Flask(__name__) 
  4.  
  5.  
  6. @app.route("/", methods=["GET"]) 
  7. def index(): 
  8.     return render_template("index.html"
  9.  
  10.  
  11. @app.route("/get-cookie/", methods=["GET"]) 
  12. def get_cookie(): 
  13.     response = make_response("Here, take some cookie!"
  14.     response.set_cookie(key="id", value="3db4adj3d"
  15.     return response 
  16.  
  17.  
  18. @app.route("/api/cities/", methods=["GET"]) 
  19. def cities(): 
  20.     if request.cookies["id"] == "3db4adj3d"
  21.         cities = [{"name""Rome""id": 1}, {"name""Siena""id": 2}] 
  22.         return jsonify(cities) 
  23.     return jsonify(msg="Ops!"

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

  1. const button = document.getElementsByTagName("button")[0]; 
  2.  
  3. button.addEventListener("click"function() { 
  4.   getACookie().then(() => getData()); 
  5. }); 
  6.  
  7. function getACookie() { 
  8.   return fetch("/get-cookie/").then(response => { 
  9.     // make sure to check response.ok in the real world! 
  10.     return Promise.resolve("All good, fetch the data"); 
  11.   }); 
  12.  
  13. function getData() { 
  14.   fetch("/api/cities/"
  15.     .then(response => { 
  16.       // make sure to check response.ok in the real world! 
  17.       return response.json(); 
  18.     }) 
  19.     .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请求传递

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

  1. FLASK_ENV=development FLASK_APP=flask_app.py flask run 

现在,在 Flask 应用程序之外的其他文件夹中,创建index.html:

  1. <!DOCTYPE html> 
  2. <html lang="en"
  3. <head> 
  4.     <meta charset="UTF-8"
  5.     <title>Title</title> 
  6. </head> 
  7. <body> 
  8. <button>FETCH</button> 
  9. </body> 
  10. <script src="index.js"></script> 
  11. </html> 

使用以下代码在同一文件夹中创建一个名为index.js的 JS 文件:

  1. button.addEventListener("click"function() { 
  2.   getACookie().then(() => getData()); 
  3. }); 
  4.  
  5. function getACookie() { 
  6.   return fetch("http://localhost:5000/get-cookie/").then(response => { 
  7.     // make sure to check response.ok in the real world! 
  8.     return Promise.resolve("All good, fetch the data"); 
  9.   }); 
  10.  
  11. function getData() { 
  12.   fetch("http://localhost:5000/api/cities/"
  13.     .then(response => { 
  14.       // make sure to check response.ok in the real world! 
  15.       return response.json(); 
  16.     }) 
  17.     .then(json => console.log(json)); 

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

  1. npx serve 

此命令为您提供了要连接的本地地址/端口,例如http://localhost:42091/。访问页面并尝试在浏览器控制台打开的情况下单击按钮。在控制台中,可以看到:

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

处理 CORSCORS

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

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

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

要解决此第一个错误,我们需要为Flask配置CORS:

  1. pip install flask-cors 

然后将 CORS 应用于 Flask:

  1. from flask import Flask, make_response, request, render_template, jsonify 
  2. from flask_cors import CORS 
  3.  
  4. app = Flask(__name__) 
  5. CORS(app=app) 
  6.  
  7.  
  8. @app.route("/", methods=["GET"]) 
  9. def index(): 
  10.     return render_template("index.html"
  11.  
  12.  
  13. @app.route("/get-cookie/", methods=["GET"]) 
  14. def get_cookie(): 
  15.     response = make_response("Here, take some cookie!"
  16.     response.set_cookie(key="id", value="3db4adj3d"
  17.     return response 
  18.  
  19.  
  20. @app.route("/api/cities/", methods=["GET"]) 
  21. def cities(): 
  22.     if request.cookies["id"] == "3db4adj3d"
  23.         cities = [{"name""Rome""id": 1}, {"name""Siena""id": 2}] 
  24.         return jsonify(cities) 
  25.     return jsonify(msg="Ops!"

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

  1. 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:

 

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

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

  1. const button = document.getElementsByTagName("button")[0]; 
  2.  
  3. button.addEventListener("click"function() { 
  4.   getACookie().then(() => getData()); 
  5. }); 
  6.  
  7. function getACookie() { 
  8.   return fetch("http://localhost:5000/get-cookie/", { 
  9.     credentials: "include" 
  10.   }).then(response => { 
  11.     // make sure to check response.ok in the real world! 
  12.     return Promise.resolve("All good, fetch the data"); 
  13.   }); 
  14.  
  15. function getData() { 
  16.   fetch("http://localhost:5000/api/cities/", { 
  17.     credentials: "include" 
  18.   }) 
  19.     .then(response => { 
  20.       // make sure to check response.ok in the real world! 
  21.       return response.json(); 
  22.     }) 
  23.     .then(json => console.log(json)); 

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

  1. fetch("http://localhost:5000/get-cookie/", { 
  2.     credentials: "include" 
  3.   }) 

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

  1. fetch("http://localhost:5000/api/cities/", { 
  2.   credentials: "include" 
  3. }) 

再试一次,我们还需要在后端修复另一个错误:

  1. 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标头。

  1. 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 属性

  1. response.set_cookie(key="id", value="3db4adj3d", secure=True

如果要在真实环境中尝试,请可以运行以下命令,并注意curl在此处是不通过HTTP保存cookie:

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

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

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

cookie jar 文件:

  1. 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 看起来是这样的:

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

在 Flask 中

  1. 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

  1. 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 称为third-party(第三方) Cookie。

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

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

使用 SameSite 属性

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

  • Strict
  • Lax
  • None

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

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

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

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

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

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

 

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

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

下面的设置无效。

  1. Set-Cookie: widget_session=abc123; SameSite=None 

下面的设置有效。

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

Cookies 和 认证

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

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

基于会话的身份验证

身份验证是 cookie 最常见的用例之一。

当你访问一个请求身份验证的网站时,后端将通过凭据提交(例如通过表单)在后台发送一个Set-Cookie标头到前端。

型的会话 cookie 如下所示:

  1. 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。

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

  • 数据库
  • 像 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标记存储在cookie或localStorage中都不是好主意。

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

总结

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

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

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

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

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

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

作者:valentinog 译者:前端小智 来源:valentinog

 

原文:https://gizmodo.com/the-complete-guide-to-cookies-and-all-the-stuff-w-1794247382

本文转载自微信公众号「大迁世界」,可以通过以下二维码关注。转载本文请联系大迁世界公众号。

 

责任编辑:武晓燕 来源: 大迁世界
相关推荐

2011-02-23 15:34:33

软件开发软件模型设计

2018-06-11 15:30:56

2018-06-27 15:14:31

HTTP编码内容

2010-08-27 15:24:56

2022-10-11 08:48:08

HTTP状态码浏览器

2018-07-12 15:30:03

HTTP缓存机制

2022-09-13 08:39:22

Bash脚本

2022-08-09 11:10:06

网络渗透测试安全漏洞

2016-09-06 22:05:41

HttpCookieWeb

2021-10-27 08:00:00

DevSecOps开发安全

2024-07-03 14:14:07

2023-11-29 09:00:00

KubernetesDevOps

2024-08-05 09:58:24

2020-09-22 12:22:32

Windows TerWindowsLinux

2023-06-11 15:51:13

2010-11-26 09:57:59

2013-07-24 13:44:25

APP开发者改变

2020-04-29 14:30:35

HTTPHTTPS前端

2021-11-23 16:00:56

DevOps价值流价值流优化

2023-12-26 08:00:00

微前端React
点赞
收藏

51CTO技术栈公众号