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=Strict 或 Lax應該控制跨站 cookie 提交。Strict 封鎖所有跨站傳送;Lax 允許頂層導航。降低 CSRF 風險。
Path=/應該將 cookie 限制於應用程式路徑。避免過於寬泛的 Domain 範圍。

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

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

不要在 session cookie 上設定 Expires 或 Max-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. 缺少 HttpOnly 或 Secure 旗標。

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

4. 沒有 session 逾時。

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

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

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

相關 BEE ​

參考資料 ​