Skip to content
BEE
Backend Engineering Essentials

[BEE-1004] 會話管理

INFO

Session(會話)是伺服器端與客戶端識別碼綁定的狀態。安全地生成 session、在傳輸中保護 session ID,以及可靠地銷毀 session,是三個不可妥協的要求。

背景

HTTP 是無狀態協定。使用者完成身份驗證後,伺服器需要一種機制在後續請求中識別同一位使用者,而不需要每次都提供憑證。傳統解法是伺服器端 session:伺服器建立一筆以隨機識別碼為鍵的記錄,儲存後將識別碼以 cookie 形式傳送給客戶端。每個後續請求都帶上該 cookie,伺服器查找記錄後即可知道是誰在發出請求。

Session 看似簡單,卻包含數個反覆出現於漏洞報告的失敗模式:

  • 可預測的 session ID 讓攻擊者能夠猜測有效的 session。
  • Session fixation(會話固定)攻擊讓攻擊者能在受害者登入前預設 session ID。
  • 缺少 cookie 安全屬性會將 session ID 暴露給 JavaScript 或明文 HTTP 截取。
  • 缺少伺服器端的 session 銷毀,意味著「登出」只是表面動作——session 依然可用。

OWASP Session Management Cheat Sheet(2024)與 NIST SP 800-63B(第 7 節,重新驗證)為本文要求提供了規範性依據。

原則

1. Session ID 必須使用密碼學安全的來源生成

Session ID 的不可預測性取決於其隨機性來源。可預測的 ID——序列計數器、時間戳記、使用者 ID——都能被攻擊者列舉。

要求:

  • 使用 CSPRNG(Cryptographically Secure Pseudorandom Number Generator,密碼學安全偽隨機數生成器)生成 session ID,絕不使用 Math.random() 或等效方式。
  • Session ID 必須攜帶至少 128 bits 的熵(OWASP 建議;NIST SP 800-63B 對隨機值的最低要求為 64 bits,128 bits 是目前的實務標準)。
  • Session ID 不得編碼任何可識別使用者的資料。識別碼必須是不透明的。

Session cookie 是 session ID 竊取的主要攻擊面。以下四個屬性在正式環境中全部必須設定。

屬性要求用途
HttpOnly必須防止 JavaScript(document.cookie)讀取 session ID。降低 XSS 竊取 session 的風險。
Secure必須將 cookie 限制於 HTTPS 連線。防止明文傳輸。(RFC 6265,第 4.1.2.5 節)
SameSite=StrictLax應該控制跨站 cookie 提交。Strict 封鎖所有跨站傳送;Lax 允許頂層導航。降低 CSRF 風險。
Path=/應該將 cookie 限制於應用程式路徑。避免過於寬泛的 Domain 範圍。

包含所有必要屬性的 Set-Cookie 標頭範例:

Set-Cookie: sessionId=a3f8c2d1e9b047f6a2d8c4e1f9b3a7d5;
            HttpOnly;
            Secure;
            SameSite=Strict;
            Path=/

不要在 session cookie 上設定 ExpiresMax-Age。沒有這些屬性的 session cookie 在瀏覽器 session 結束時即過期,縮短了被竊 cookie 可被重放的時間窗口。

3. 驗證後必須重新生成 Session ID(防止 session fixation)

Session fixation 是一種攻擊,攻擊者在受害者驗證前建立已知的 session ID,等受害者登入後接管其 session。防禦措施是無條件的:成功登入後立即丟棄驗證前的 session ID,發放新的 ID。

任何權限提升後也應套用相同的重新生成機制(例如,使用者確認密碼以存取敏感操作)。

4. 登出與逾時時必須在伺服器端銷毀 Session

僅刪除 cookie 的客戶端登出是不夠的。伺服器必須在其 session store 中刪除或標記該 session 記錄為無效。登出後重放的被竊 cookie 必須被拒絕。

應獨立執行兩種逾時策略:

  • 閒置逾時(Idle timeout):在一段閒置期後銷毀 session(標準應用程式建議 15–30 分鐘;高權限操作建議 2–5 分鐘)。NIST SP 800-63B 要求在 AAL2 等級下閒置 30 分鐘後重新驗證。
  • 絕對逾時(Absolute timeout):不論活動狀況,在固定時鐘時間後銷毀 session(標準應用程式建議 8–24 小時;敏感情境建議 4–8 小時)。NIST SP 800-63B 要求在 AAL2 等級下至少每 12 小時重新驗證一次。

兩種逾時都必須在伺服器端執行。客戶端倒數計時僅為參考 UI。

5. Session 儲存必須支援可靠的查找與刪除

概念層面的三種常見方式:

  • 進程內記憶體(In-process memory):速度快,無基礎設施成本;伺服器重啟後所有 session 消失;無法跨多個實例擴展。
  • 資料庫儲存(Database-backed):session 在重啟後持續存在,可跨實例運作;需要在 session ID 欄位建立索引;透過 TTL 或清理任務支援過期。
  • 分散式快取(Distributed cache):讀取速度快,內建 TTL 過期,可水平擴展;引入外部依賴;需要快取層的高可用性。

選擇取決於擴展性要求。多實例部署需要共享儲存(資料庫或快取),或搭配負載均衡器的 sticky session。沒有共享儲存的 sticky session 會使跨叢集的伺服器端銷毀變得複雜。

6. Session 與 token:選擇合適的工具

Session 與 token(JWT)不可互換。主要的取捨在於有狀態性與擴展性:

面向伺服器端 session無狀態 token(JWT)
撤銷即時——刪除 session 記錄困難——若無封鎖清單,token 在過期前持續有效
伺服器狀態必要(session store)無(透過簽名驗證)
擴展性大規模需要共享 session store每個節點本地驗證
Payload 大小極小的 cookie(僅 ID)Token 攜帶所有 claims
可稽核性容易——伺服器日誌包含 session 事件需要彙整 token 呈現的日誌

當即時撤銷是必要條件時(例如金融應用程式、管理控制台),使用伺服器端 session。當水平擴展且無共享狀態是優先考量,且短暫的過期時間窗口(通常 15 分鐘)是可接受的撤銷延遲時,使用無狀態 token。Token 設計請參見 BEE-1002

視覺化

以下圖表展示完整的 session 生命週期,從登入到登出與逾時。

範例

以下虛擬碼展示登入時建立 session 與每次後續請求的驗證流程。刻意採用框架無關的寫法。

# --- Login handler ---
function handle_login(request):
    credentials = parse_credentials(request)
    user = authenticate(credentials)
    if user is null:
        return response(401, "Invalid credentials")

    # Discard any pre-login session (session fixation prevention)
    old_session_id = read_cookie(request, "sessionId")
    if old_session_id is not null:
        session_store.delete(old_session_id)

    # Create new session with fresh cryptographic ID
    session_id   = csprng.generate_hex(32)          # 256 bits
    session_data = {
        user_id:     user.id,
        created_at:  now(),
        last_active: now(),
    }
    session_store.set(session_id, session_data, ttl=absolute_timeout)

    response = response(200, "Logged in")
    response.set_cookie(
        name     = "sessionId",
        value    = session_id,
        http_only = true,
        secure   = true,
        same_site = "Strict",
        path     = "/",
        # No Expires / Max-Age — session cookie
    )
    return response


# --- Request validation middleware ---
function require_session(request):
    session_id = read_cookie(request, "sessionId")
    if session_id is null:
        return response(401, "No session")

    session = session_store.get(session_id)
    if session is null:
        return response(401, "Session not found or expired")

    idle_seconds = now() - session.last_active
    if idle_seconds > idle_timeout:
        session_store.delete(session_id)
        return response(401, "Session expired (idle timeout)")

    # Refresh last_active
    session.last_active = now()
    session_store.set(session_id, session, ttl=absolute_timeout)

    request.user_id = session.user_id
    return next(request)


# --- Logout handler ---
function handle_logout(request):
    session_id = read_cookie(request, "sessionId")
    if session_id is not null:
        session_store.delete(session_id)         # Server-side invalidation

    response = response(200, "Logged out")
    response.set_cookie(
        name    = "sessionId",
        value   = "",
        max_age = 0,                             # Instruct browser to delete cookie
    )
    return response

常見錯誤

1. 使用可預測的 session ID。

序列整數、使用者 ID 和時間戳記都不是 session ID。任何攻擊者能計算或列舉的值都是可列舉的。只使用 CSPRNG 輸出。

2. 登入後未重新生成 session ID。

若伺服器在驗證邊界前後重用相同的 session ID,攻擊者在登入前植入已知 session ID 後,即可在受害者登入後接管其 session。每次成功驗證後必須無條件重新生成。

3. 缺少 HttpOnlySecure 旗標。

沒有 HttpOnly 的 session cookie 可被注入的 JavaScript 竊取。沒有 Secure 的 cookie 可在 HTTP 明文頁面或攻擊者控制的網路上被截取。兩個旗標在正式環境中都是必要的;沒有任何正當理由可以缺少其中一個。

4. 沒有 session 逾時。

沒有閒置或絕對逾時的 session 永遠有效,或直到使用者主動登出。如果使用者從未登出(關閉分頁、裝置被竊),session 將無限期地可被利用。實作伺服器端執行的閒置和絕對逾時。

5. 登出時未在伺服器端銷毀 session。

清除 session cookie 而不刪除伺服器端記錄,會讓 session 繼續可用。任何在登出前截取到 session ID 的攻擊者都可以繼續使用它。登出必須先刪除 session store 中的 session 記錄,再清除 cookie。

相關 BEE

參考資料