National Central University
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try an AI Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

3

Local AI

--

Uedu Code

--

中央大學 AQI 80 24°C PM2.5 21
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 14

Cookie 與 Session

HTTP 不記得你:Cookie 怎麼運作、Flask 的 Session 為什麼有簽章
Interaction實作 30 minute(s)

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:

  1. 伺服器在回應加上 Set-Cookie 標頭,請瀏覽器記住一個值
  2. 瀏覽器之後每次送請求到這個網站,都在 Cookie 標頭把值帶回來

伺服器還是不記得你,只是瀏覽器每次都會自己報上名字。

逐步看:Cookie 的一生

browser_a、browser_b 是兩個 test_client,就像兩個不同的瀏覽器,各自保管自己的 Cookie。注意 index 什麼時候讀得到 theme。

逐步看:Cookie 的屬性

比較三個 Set-Cookie 的差別。最後兩行看 path 的效果:同一個瀏覽器,開不同的網址帶的 Cookie 不一樣。

三個和安全有關的屬性

  • HttpOnly(httponly=True):頁面的 JavaScript 讀不到,document.cookie 看不到。就算網站有 XSS,也偷不到這個 Cookie
  • Secure(secure=True):只有 HTTPS 才送,不會在沒有加密的連線上傳。localhost 通常例外,所以本機測試不受影響
  • SameSite(samesite='Lax'):別的網站發出的請求要不要帶這個 Cookie。Strict 都不帶;Lax 只有使用者點連結過來時帶;None 都帶,而且必須同時有 Secure

幾個容易誤會的地方:

  • 沒寫 SameSite 時,各瀏覽器不一定相同。 MDN 的說法是部分瀏覽器預設當成 Lax,所以要的行為請明確寫出來,不要依賴預設值
  • path 不是安全機制。 MDN 明確說它不能防止其他路徑讀取,只是控制什麼時候送
  • 這些屬性都管不到使用者自己。 Cookie 存在使用者的電腦上,他隨時可以用開發者工具修改。下面的練習會實際改一次

Flask 的 session:簽章過的 Cookie

剛才的問題是:Cookie 由使用者保管,不能相信裡面的值。Flask 內建的 session 用簽章解決這件事。

app.secret_key = '只有伺服器知道的字串'

session['course'] = 'python'        # 用起來像字典
session.get('course')

Flask 把字典轉成文字放進名為 session 的 Cookie,後面加上用 secret_key 算出來的簽章。下次請求回來時,再算一次比對:內容被改過,簽章就對不上,Flask 會當作沒有 session。

要注意兩件事,預設情況下它不存在伺服器,也沒有加密:

  • 內容存在瀏覽器的 Cookie 裡,任何人都可以解開來看,所以不要放密碼或機密
  • 簽章只保證「內容是這個伺服器發出的、沒有被改過」

下面的動畫實際做一次:解開內容、竄改、換金鑰。

逐步看:看得到,改不了

注意第一行輸出的 Cookie 是用兩個點分成三段:內容、時間戳記、簽章。

互動練習:改一個字、換一把金鑰

這次資料放在 Flask 的 session。「Session」面板分成兩塊:上面是 Cookie 裡看得到的內容(中文會顯示成 \uXXXX),下面是伺服器驗證簽章後讀到的 session。

Cookie 的值很長,改的時候只要動第一個字元就好。這些動作都是在你自己的瀏覽器裡對模擬的網站操作。

任務四會用到 session 的期限。預設沒有 Max-Age,是 Session Cookie,關掉瀏覽器就結束(MDN 提醒,有些瀏覽器重新開啟時會還原工作階段,這種 Cookie 可能活得比預期久)。要讓它活久一點:

from datetime import timedelta

app.permanent_session_lifetime = timedelta(days=7)    # 預設是 31 天
session.permanent = True                              # 通常在登入成功時設定

這不只是請瀏覽器保留,Flask 驗證簽章時也會檢查時間戳記,超過期限的 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 門課(最新的在最前面)。程式最下面的測試不要改。

你的輸出
預期輸出

想一想

1.

Flask 預設的 session,下面哪一句正確?

2.

下面的程式會印出什麼?

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)
3.

網站有 XSS 漏洞,攻擊者想用 document.cookie 偷走 Cookie。哪個屬性能擋下這件事?

4.

把 secret_key 換掉之後會發生什麼事?

5.

下面哪一種資料最不適合直接放在 Flask 預設的 session?

第 1 次作答

重點整理

  • 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。

這一課完成了!
Next lesson:註冊與登入
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。