Learning Objectives
- 說明 HTTP 無狀態的意思,以及 Cookie 怎麼補上這個缺口
- 用 set_cookie、request.cookies、delete_cookie 操作 Cookie,說明 max_age、path、HttpOnly、Secure、SameSite
- 說明 Flask 的 session 存在哪裡、為什麼看得到內容卻改不了、換掉 secret_key 會發生什麼事
- 比較把資料放在瀏覽器與放在伺服器的取捨
HTTP 不記得你
HTTP 是無狀態(stateless)的:每個請求都是獨立的,伺服器處理完就忘了。你剛剛選了深色主題,下一個請求進來時,對伺服器來說又是一個陌生人。
這樣設計讓伺服器很簡單,但網站需要記住事情:選了什麼主題、購物車有什麼、登入的是誰。解法是 Cookie:
- 伺服器在回應加上
Set-Cookie標頭,請瀏覽器記住一個值 - 瀏覽器之後每次送請求到這個網站,都在
Cookie標頭把值帶回來
伺服器還是不記得你,只是瀏覽器每次都會自己報上名字。
逐步看:Cookie 的一生
browser_a、browser_b 是兩個 test_client,就像兩個不同的瀏覽器,各自保管自己的 Cookie。注意 index 什麼時候讀得到 theme。
逐步看:Cookie 的屬性
比較三個 Set-Cookie 的差別。最後兩行看 path 的效果:同一個瀏覽器,開不同的網址帶的 Cookie 不一樣。
三個和安全有關的屬性
HttpOnly(httponly=True):頁面的 JavaScript 讀不到,document.cookie看不到。就算網站有 XSS,也偷不到這個 CookieSecure(secure=True):只有 HTTPS 才送,不會在沒有加密的連線上傳。localhost 通常例外,所以本機測試不受影響SameSite(samesite='Lax'):別的網站發出的請求要不要帶這個 Cookie。Strict都不帶;Lax只有使用者點連結過來時帶;None都帶,而且必須同時有Secure
幾個容易誤會的地方:
Flask 的 session:簽章過的 Cookie
剛才的問題是:Cookie 由使用者保管,不能相信裡面的值。Flask 內建的 session 用簽章解決這件事。
app.secret_key = '只有伺服器知道的字串'
session['course'] = 'python' # 用起來像字典
session.get('course')
Flask 把字典轉成文字放進名為 session 的 Cookie,後面加上用 secret_key 算出來的簽章。下次請求回來時,再算一次比對:內容被改過,簽章就對不上,Flask 會當作沒有 session。
要注意兩件事,預設情況下它不存在伺服器,也沒有加密:
- 內容存在瀏覽器的 Cookie 裡,任何人都可以解開來看,所以不要放密碼或機密
- 簽章只保證「內容是這個伺服器發出的、沒有被改過」
下面的動畫實際做一次:解開內容、竄改、換金鑰。
逐步看:看得到,改不了
注意第一行輸出的 Cookie 是用兩個點分成三段:內容、時間戳記、簽章。
放在瀏覽器,還是放在伺服器
另一種做法是 Cookie 裡只放一個隨機的編號,內容留在伺服器(資料庫或 Redis):
| 簽章 Cookie(Flask 預設) | 伺服器端 Session | |
|---|---|---|
| 內容存在 | 瀏覽器 | 伺服器(資料庫、Redis) |
| 使用者看得到內容 | 看得到 | 看不到,只有編號 |
| 大小限制 | 約 4 KB(Flask 超過 4093 位元組會警告) | 幾乎沒有 |
| 伺服器要多存東西 | 不用,多台伺服器也不用共用 | 要,多台伺服器要共用儲存 |
| 立刻讓某個人登出 | 做不到,只能換金鑰讓所有人失效,或另外記錄 | 刪掉那筆就好 |
小網站用 Flask 預設的就夠了。需要存放較多資料、或需要強制登出時,可以用 Flask-Session 這類擴充改存伺服器。
不論哪一種,都要遵守:secret_key 不能外流(拿到它的人可以偽造任何人的 session),也不要寫死在程式裡進版本控制,第 19 課會說明怎麼從環境變數讀取。可以用下面的指令產生一把:
python -c "import secrets; print(secrets.token_hex())"
上線前還要注意(Flask 設定文件):SESSION_COOKIE_HTTPONLY 預設開啟,SESSION_COOKIE_SECURE、SESSION_COOKIE_SAMESITE 預設沒有開,要自己設定:
app.config.update(SESSION_COOKIE_SECURE=True, SESSION_COOKIE_SAMESITE='Lax')
容易中招的一點: session['items'].append(x) 改的是串列裡面,Flask 看不出字典有變,不會發新的 Cookie。要嘛把值重新指定一次(session['items'] = items),要嘛加上 session.modified = True。
動手做:主題與最近看過的課
完成三個地方:/theme/<name> 設定 Cookie,/whoami 讀 Cookie,/view/<course> 用 session 記住最近看過的 3 門課(最新的在最前面)。程式最下面的測試不要改。
想一想
Flask 預設的 session,下面哪一句正確?
下面的程式會印出什麼?
from flask import Flask, session
app = Flask(__name__)
app.secret_key = 'dev'
@app.route('/start')
def start():
session['items'] = ['a']
return 'ok'
@app.route('/add')
def add():
session['items'].append('b')
return 'ok'
@app.route('/show')
def show():
return ','.join(session['items'])
client = app.test_client()
client.get('/start')
client.get('/add')
print(client.get('/show').text)
網站有 XSS 漏洞,攻擊者想用 document.cookie 偷走 Cookie。哪個屬性能擋下這件事?
把 secret_key 換掉之後會發生什麼事?
下面哪一種資料最不適合直接放在 Flask 預設的 session?
重點整理
- HTTP 無狀態;Cookie 由伺服器用
Set-Cookie交給瀏覽器,瀏覽器之後每次帶回來 set_cookie要有回應物件;delete_cookie是請瀏覽器讓它過期;max_age的單位是秒HttpOnly防 JavaScript 讀取,Secure限 HTTPS,SameSite管跨站請求;不要依賴瀏覽器的預設值- 一般 Cookie 可以被使用者修改,不能相信裡面的值
- Flask 的 session 是簽章過的 Cookie:看得到、改了會失效;換
secret_key全部失效;不要放機密 - 改 session 裡的串列後要重新指定,或設
session.modified = True
現在網站可以記住「這個瀏覽器」了。但是 session 裡要放的是誰?下一課註冊與登入處理這件事:確認對方真的是他自稱的人,再把使用者編號放進 session。