Cookie 機制完全解析:從屬性到安全
Cookie 是 HTTP 無狀態協定中維持狀態的關鍵機制。本篇將全面解析 Cookie 的各個面向。
一、 Cookie 基礎
1.1 什麼是 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"二、 Cookie 屬性詳解
2.1 屬性一覽
| 屬性 | 說明 | 預設值 |
|---|---|---|
Name=Value | Cookie 名稱和值 | 必填 |
Domain | 可存取的域 | 當前域 |
Path | 可存取的路徑 | 當前路徑 |
Expires | 過期時間(日期) | Session |
Max-Age | 有效期(秒) | Session |
Secure | 只在 HTTPS 傳輸 | 無 |
HttpOnly | JavaScript 無法存取 | 無 |
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四、 第三方 Cookie
4.1 什麼是第三方 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 | 預設封鎖追蹤類 |
| Chrome | 2024 年起逐步淘汰 |
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 等五、 Cookie 安全
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: "/",
});5.2 Cookie 前綴
javascript
// __Secure- 前綴:必須設置 Secure
res.cookie("__Secure-token", "abc", {
secure: true, // 必須
});
// __Host- 前綴:更嚴格
res.cookie("__Host-session", "xyz", {
secure: true, // 必須
path: "/", // 必須是 /
// domain: xxx // 不能設置 domain
});5.3 Cookie 簽名
javascript
const cookieParser = require("cookie-parser");
// 使用密鑰
app.use(cookieParser("my-secret-key"));
// 設置簽名 Cookie
res.cookie("user", "john", { signed: true });
// 讀取
req.signedCookies.user; // 'john' 或 false(如果被篡改)六、 Cookie 限制
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 | 防止 CSRF | Strict 或 Lax |
Max-Age | 過期時間 | 依需求設定 |
Path | 限制路徑 | 需要時設定 |
Domain | 設定域 | 謹慎使用 |
> **安全 Cookie 公式**:`HttpOnly + Secure + SameSite=Strict`
進階挑戰
- 實作一個 Cookie 同意橫幅,符合 GDPR 要求。
- 研究瀏覽器的 Storage Access API,了解如何處理第三方 Cookie 限制。
- 比較 Cookie、localStorage、sessionStorage 的使用場景。