作者:valentinog
譯者:前端小智
來源:valentinog
有夢想,有乾貨,微信搜尋 【大遷世界】 關注這個在凌晨還在刷碗的刷碗智。
本文 GitHub https://github.com/qq449245884/xiaozhi 已收錄,有一線大廠面試完整考點、資料以及我的系列文章。
我們正在招聘,年薪:25-60萬+獎金,有興趣的可以點我瞭解詳情。
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
:
或者可以在開發人員工具中選中Storage
選項卡。單擊cookie,會看到 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
在 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,我們可以通過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
在另一個終端中,如果我們與根路由建立連線,則可以在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/
路由並儲存 cookit
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沒有)
主機不匹配(子域名)
檢視 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是使用 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的子域的請求
Cookies 和公共字尾列表
檢視 https://serene-bastion-01422.herokuapp.com/get-domain-cookie/:
設定的 cookie:
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:
Set-Cookie: coookiename=subd0m41n-c00k13
當域在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 不會隨請求一起出現:
概括地說,瀏覽器使用以下啟發式規則來決定如何處理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:
為了在不同來源的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 稱為third-party
(第三方) Cookie。
第三方 Cookie 除了用於 CSRF 攻擊,還可以用於使用者追蹤。比如,Facebook 在第三方網站插入一張看不見的圖片。
![](facebook.com)
瀏覽器載入上面程式碼時,就會向 Facebook 發出帶有 Cookie 的請求,從而 Facebook 就會知道你是誰,訪問了什麼網站。
使用 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 表單。詳見下表。
設定了Strict
或Lax
以後,基本就杜絕了 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
頭中,伺服器可以包括一個名為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配置
- 不攜帶敏感資料
人才們的 【三連】 就是小智不斷分享的最大動力,如果本篇部落格有任何錯誤和建議,歡迎人才們留言,最後,謝謝大家的觀看。
程式碼部署後可能存在的BUG沒法實時知道,事後為了解決這些BUG,花了大量的時間進行log 除錯,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug。
原文:https://gizmodo.com/the-compl...
交流
有夢想,有乾貨,微信搜尋 【大遷世界】 關注這個在凌晨還在刷碗的刷碗智。
本文 GitHub https://github.com/qq44924588... 已收錄,有一線大廠面試完整考點、資料以及我的系列文章。