跳至主要內容
Skip to content

Cookie 機制完全解析:從屬性到安全

Cookie 是 HTTP 無狀態協定中維持狀態的關鍵機制。本篇將全面解析 Cookie 的各個面向。


Cookie 是伺服器發送給瀏覽器的小型文字資料,瀏覽器會在後續請求中自動帶回:

1.2 設置與讀取

伺服器端設置

javascript
// Express
res.cookie("name", "value", {
  maxAge: 86400000, // 1 天(毫秒)
  httpOnly: true,
  secure: true,
  sameSite: "strict",
});

// 原生 Node.js
res.setHeader("Set-Cookie", "name=value; Max-Age=86400; HttpOnly; Secure");

客戶端設置

javascript
// 只能設置非 HttpOnly 的 Cookie
document.cookie = "name=value; max-age=86400; path=/";

// 讀取
console.log(document.cookie); // "name=value; other=data"

2.1 屬性一覽

屬性說明預設值
Name=ValueCookie 名稱和值必填
Domain可存取的域當前域
Path可存取的路徑當前路徑
Expires過期時間(日期)Session
Max-Age有效期(秒)Session
Secure只在 HTTPS 傳輸
HttpOnlyJavaScript 無法存取
SameSite跨站請求限制Lax(現代瀏覽器)

2.2 Domain 屬性

javascript
// 設置在 app.example.com

// 預設:只有 app.example.com 可以存取
res.cookie("token", "abc");

// 設置 domain:example.com 及其子域都可存取
res.cookie("token", "abc", { domain: ".example.com" });
// api.example.com ✅
// admin.example.com ✅
// other.com ❌

2.3 Path 屬性

javascript
// 只在 /api 路徑下發送
res.cookie("api_token", "xyz", { path: "/api" });

// /api/users ✅
// /api/posts ✅
// /dashboard ❌

2.4 過期時間

javascript
// Session Cookie(關閉瀏覽器就消失)
res.cookie("session", "abc");

// 持久 Cookie
res.cookie("remember", "token", {
  maxAge: 30 * 24 * 60 * 60 * 1000, // 30 天
});

// 或使用 Expires
res.cookie("remember", "token", {
  expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
});

// 刪除 Cookie
res.cookie("remember", "", { maxAge: 0 });
// 或
res.clearCookie("remember");

2.5 Secure 屬性

javascript
// 只在 HTTPS 連線中傳輸
res.cookie("secure_data", "secret", { secure: true });

// ⚠️ 開發環境 localhost 通常視為安全
// 生產環境必須設置

2.6 HttpOnly 屬性

javascript
// JavaScript 無法讀取,防止 XSS 竊取
res.cookie("session", "abc", { httpOnly: true });

// 前端
document.cookie; // 看不到 httpOnly 的 Cookie

三、 SameSite 屬性

3.1 三種值

說明跨站行為
Strict最嚴格完全不發送
Lax寬鬆(預設)Top-level 導航時發送
None無限制總是發送(需要 Secure)

3.2 行為比較

3.3 使用場景

javascript
// Session Cookie:防止 CSRF
res.cookie("session", "abc", {
  httpOnly: true,
  secure: true,
  sameSite: "strict", // 或 'lax'
});

// 跨站 Cookie(需要在第三方網站使用)
res.cookie("widget_token", "xyz", {
  httpOnly: true,
  secure: true, // 必須!
  sameSite: "none",
});

3.4 Lax vs Strict

javascript
// 場景:用戶從 Google 點擊連結進入你的網站

// Strict:用戶需要重新登入
// 因為從 google.com 導航來,Cookie 不會發送

// Lax:用戶已登入狀態
// Top-level 導航(點擊連結)會發送 Cookie

html
<!-- 當前頁面: https://example.com -->

<!-- 第一方 Cookie:example.com 設置的 -->
<!-- 第三方 Cookie:其他域設置的 -->

<!-- 廣告追蹤 -->
<img src="https://ads.tracker.com/pixel.gif" />
<!-- tracker.com 可以設置 Cookie -->

<!-- 社交分享 -->
<iframe src="https://facebook.com/like-button"></iframe>
<!-- facebook.com 可以設置 Cookie -->

4.2 瀏覽器限制

瀏覽器第三方 Cookie 政策
Safari預設封鎖
Firefox預設封鎖追蹤類
Chrome2024 年起逐步淘汰

4.3 替代方案

javascript
// 1. 使用第一方 Cookie + 後端代理
// 前端
fetch("/api/analytics", { credentials: "include" });

// 後端代理到分析服務
app.post("/api/analytics", (req, res) => {
  // 轉發到分析服務,使用 API Key
});

// 2. 使用 Storage API
localStorage.setItem("user_id", "abc123");

// 3. 使用瀏覽器新 API
// Topics API、Attribution Reporting API 等

5.1 安全設置範本

javascript
// Session Cookie
res.cookie("session", sessionId, {
  httpOnly: true, // 防止 XSS 讀取
  secure: true, // 只在 HTTPS
  sameSite: "strict", // 防止 CSRF
  maxAge: 3600000, // 1 小時
  path: "/",
});

// Remember Me Token
res.cookie("remember", token, {
  httpOnly: true,
  secure: true,
  sameSite: "lax", // 允許 top-level 導航
  maxAge: 30 * 24 * 60 * 60 * 1000, // 30 天
  path: "/",
});
javascript
// __Secure- 前綴:必須設置 Secure
res.cookie("__Secure-token", "abc", {
  secure: true, // 必須
});

// __Host- 前綴:更嚴格
res.cookie("__Host-session", "xyz", {
  secure: true, // 必須
  path: "/", // 必須是 /
  // domain: xxx   // 不能設置 domain
});
javascript
const cookieParser = require("cookie-parser");

// 使用密鑰
app.use(cookieParser("my-secret-key"));

// 設置簽名 Cookie
res.cookie("user", "john", { signed: true });

// 讀取
req.signedCookies.user; // 'john' 或 false(如果被篡改)

6.1 大小與數量

限制
單一 Cookie 大小約 4KB
每個域的 Cookie 數量約 50 個
總 Cookie 大小依瀏覽器而定

6.2 超過限制的處理

javascript
// ❌ Cookie 太大
res.cookie("data", hugeJsonString); // 可能失敗

// ✅ 只存 ID,資料放伺服器
res.cookie("session", sessionId);
// 伺服器用 sessionId 查找完整資料

// ✅ 或使用其他儲存
// localStorage: 5-10MB
// IndexedDB: 更大

七、 實戰:完整登入流程

7.1 後端實作

javascript
const express = require("express");
const cookieParser = require("cookie-parser");
const jwt = require("jsonwebtoken");

app.use(cookieParser());

// 登入
app.post("/login", async (req, res) => {
  const { email, password } = req.body;
  const user = await validateCredentials(email, password);

  if (!user) {
    return res.status(401).json({ error: "Invalid credentials" });
  }

  const token = jwt.sign({ userId: user.id }, SECRET, { expiresIn: "1h" });

  res.cookie("token", token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    sameSite: "strict",
    maxAge: 3600000,
  });

  res.json({ user: { id: user.id, email: user.email } });
});

// 登出
app.post("/logout", (req, res) => {
  res.clearCookie("token", {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    sameSite: "strict",
  });
  res.json({ success: true });
});

// 認證中介軟體
function authenticate(req, res, next) {
  const token = req.cookies.token;

  if (!token) {
    return res.status(401).json({ error: "Not authenticated" });
  }

  try {
    const decoded = jwt.verify(token, SECRET);
    req.userId = decoded.userId;
    next();
  } catch (err) {
    res.clearCookie("token");
    res.status(401).json({ error: "Invalid token" });
  }
}

// 受保護路由
app.get("/profile", authenticate, async (req, res) => {
  const user = await User.findById(req.userId);
  res.json(user);
});

7.2 前端實作

javascript
// 登入
async function login(email, password) {
  const response = await fetch("/api/login", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    credentials: "include", // 重要!
    body: JSON.stringify({ email, password }),
  });
  return response.json();
}

// 請求受保護資源
async function getProfile() {
  const response = await fetch("/api/profile", {
    credentials: "include", // Cookie 自動帶上
  });
  return response.json();
}

// 登出
async function logout() {
  await fetch("/api/logout", {
    method: "POST",
    credentials: "include",
  });
}

總結

屬性用途建議
HttpOnly防止 XSS敏感 Cookie 必設
Secure只在 HTTPS生產環境必設
SameSite防止 CSRFStrict 或 Lax
Max-Age過期時間依需求設定
Path限制路徑需要時設定
Domain設定域謹慎使用

> **安全 Cookie 公式**:`HttpOnly + Secure + SameSite=Strict`


進階挑戰

  1. 實作一個 Cookie 同意橫幅,符合 GDPR 要求。
  2. 研究瀏覽器的 Storage Access API,了解如何處理第三方 Cookie 限制。
  3. 比較 Cookie、localStorage、sessionStorage 的使用場景。

延伸閱讀與資源