TypeScript 中的 `Cookie`

TypeScript 中的 `Cookie`

本文說明 TypeScript 中的 Cookie

我們會逐步介紹在瀏覽器與伺服器上安全且可靠地處理 Cookie 的實用模式。

YouTube Video

TypeScript 中的 Cookie

Cookie 的基本概念

Cookie 是在用戶端儲存小字串(name=value 配對)的機制,可透過 Set-Cookie HTTP 標頭或 document.cookie 建立。可以用安全屬性(HttpOnlySecureSameSite 等)來控制其行為。

瀏覽器中的基本操作:document.cookie

以下是在瀏覽器中寫入 Cookie 的最小範例。將字串附加到 document.cookie 以建立 Cookie。

 1// Set a simple cookie that expires in 7 days.
 2// Note: Comments are in English per the user's preference.
 3const setSimpleCookie = (name: string, value: string) => {
 4  const days = 7;
 5  const expires = new Date(Date.now() + days * 86400_000).toUTCString();
 6  // name=value; Expires=...; Path=/
 7  document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; Expires=${expires}; Path=/`;
 8};
 9
10setSimpleCookie('theme', 'dark');
  • 此程式碼會建立一個 7 天後到期的 Cookie。在瀏覽器端使用起來很容易,但由於無法設定 HttpOnly,應避免儲存敏感資訊。

讀取 Cookie(瀏覽器)

以下函式是一個輔助工具,從 document.cookie 取得指定名稱的 Cookie 值。document.cookie 會以由分號與空白('; ')分隔的字串形式回傳。因此,我們將其分割後,尋找目標 Cookie。

 1// Parse document.cookie and return value for given name or null if not found.
 2const getCookie = (name: string): string | null => {
 3  const cookies = document.cookie ? document.cookie.split('; ') : [];
 4  for (const cookie of cookies) {
 5    const [k, ...rest] = cookie.split('=');
 6    const v = rest.join('=');
 7    if (decodeURIComponent(k) === name) {
 8      return decodeURIComponent(v);
 9    }
10  }
11  return null;
12};
13
14// Example usage:
15const theme = getCookie('theme'); // => "dark" if set
16console.log('theme cookie:', theme);
  • 此函式會對 Cookie 名稱與值進行解碼,以安全地比較並取得它們。它能處理重複的 Cookie 名稱與經編碼的特殊字元,因此是簡潔但穩健的實作。

理解並套用安全屬性

Cookie 有幾個重要屬性,每個屬性用來控制安全性與行為範圍。

  • HttpOnly 屬性可防止 JavaScript 存取該 Cookie,有助於緩解 XSS(跨站腳本)攻擊。請注意,無法在瀏覽器端設定 HttpOnly
  • Secure 屬性限制 Cookie 僅能透過 HTTPS 傳送,降低被竊聽與篡改的風險。
  • SameSite 屬性控制 Cookie 是否會隨跨站請求一同送出,並有助於防止 CSRF(跨站請求偽造)攻擊。
  • Path 屬性指定 Cookie 會隨哪些請求路徑送出,而 Domain 屬性則指定 Cookie 有效的網域。
  • 透過設定 ExpiresMax-Age 屬性,可以控制 Cookie 的到期時間。

在瀏覽器中加入 SameSiteSecure 的範例(能做到的部分)

HttpOnly 無法在用戶端設定。以下是在用戶端加入 SameSiteSecure 的範例。但 Secure 僅會在 HTTPS 頁面上生效。

 1// Set cookie with SameSite and Secure attributes (Secure only effective over HTTPS).
 2const setCookieWithAttributes = (name: string, value: string) => {
 3  const maxAge = 60 * 60 * 24 * 7; // 7 days in seconds
 4  // Note: HttpOnly cannot be set from JS; set it on server-side when you want to restrict JS access.
 5  document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; `
 6    + `Max-Age=${maxAge}; `
 7    + `Path=/; `
 8    + `SameSite=Lax; `
 9    + `Secure`;
10};
11
12setCookieWithAttributes('session_hint', 'true');
  • SameSite=Lax 是安全且常見的選擇。若需更嚴格的限制可使用 SameSite=Strict,但可能使來自外部網站的合法導覽無法運作。

在伺服器上設定 Cookie(Set-Cookie)(Node/TypeScript)

在伺服器端可透過 Set-Cookie 標頭加入 HttpOnly,因此工作階段管理理想上應由伺服器端處理。以下是使用 Node 的 http 模組的範例。

 1// A minimal Node HTTP server in TypeScript that sets a secure HttpOnly cookie.
 2// This example uses built-in 'crypto' for a random session id.
 3import http from 'http';
 4import crypto from 'crypto';
 5
 6const server = http.createServer((req, res) => {
 7  if (req.url === '/login') {
 8    const sessionId = crypto.randomBytes(16).toString('hex');
 9    // Set cookie with HttpOnly, Secure, SameSite and Path
10    // Expires is optional — Max-Age preferred for relative lifetimes.
11    res.setHeader('Set-Cookie', `sid=${sessionId}; `
12      + `HttpOnly; `
13      + `Secure; `
14      + `SameSite=Strict; `
15      + `Path=/; `
16      + `Max-Age=3600`
17    );
18    res.writeHead(302, { Location: '/' });
19    res.end();
20    return;
21  }
22
23  res.writeHead(200, { 'Content-Type': 'text/plain' });
24  res.end('Hello\n');
25});
26
27server.listen(3000, () => {
28  console.log('Server running on http://localhost:3000');
29});
  • 當你在伺服器端設定 HttpOnly 屬性後,Cookie 將無法被 JavaScript 存取,使 XSS(跨站腳本)攻擊更難竊取它。另外,加入 Secure 屬性讓 Cookie 僅透過 HTTPS 傳送,可防止竊聽與篡改,提升通訊安全性。

Cookie 序列化/解析的實作(伺服器端輔助工具)

以下是一個在伺服器端產生 Set-Cookie 標頭字串的簡易工具。

 1// Cookie serialization helper for server-side use.
 2// Returns a properly formatted Set-Cookie header value.
 3type CookieOptions = {
 4  path?: string;
 5  domain?: string;
 6  maxAge?: number;
 7  expires?: Date;
 8  httpOnly?: boolean;
 9  secure?: boolean;
10  sameSite?: 'Strict' | 'Lax' | 'None';
11};
12
13const serializeCookie = (name: string, value: string, opts: CookieOptions = {}): string => {
14  const parts: string[] = [`${encodeURIComponent(name)}=${encodeURIComponent(value)}`];
15
16  if (opts.maxAge != null) parts.push(`Max-Age=${Math.floor(opts.maxAge)}`);
17  if (opts.expires) parts.push(`Expires=${opts.expires.toUTCString()}`);
18  if (opts.domain) parts.push(`Domain=${opts.domain}`);
19  parts.push(`Path=${opts.path ?? '/'}`);
20  if (opts.httpOnly) parts.push('HttpOnly');
21  if (opts.secure) parts.push('Secure');
22  if (opts.sameSite) parts.push(`SameSite=${opts.sameSite}`);
23
24  return parts.join('; ');
25};
26
27// Example usage:
28const headerValue = serializeCookie('uid', 'abc123', {
29  httpOnly: true,
30  secure: true,
31  sameSite: 'Lax',
32  maxAge: 3600
33});
34console.log(headerValue);
35// => "uid=abc123; Max-Age=3600; Path=/; HttpOnly; Secure; SameSite=Lax"
  • 此工具可作為自訂伺服器建立 Set-Cookie 的基礎。程式庫能處理更多邊界案例。

用簽章(HMAC)防止竄改

直接把重要值存入 Cookie 很危險。我們介紹一種在伺服器端替值簽章以偵測竄改的方法。此處使用 HMAC-SHA256。在生產環境中,您還需要在伺服器端管理 Token 的撤銷。

 1// A simple HMAC signing and verification helper for cookies.
 2// Signing prevents client from tampering cookie values.
 3import crypto from 'crypto';
 4
 5const SECRET = 'replace_with_env_secret'; // store in env var in production
 6
 7const signValue = (value: string) => {
 8  const hmac = crypto.createHmac('sha256', SECRET);
 9  hmac.update(value);
10  return `${value}.${hmac.digest('hex')}`;
11};
12
13const verifySignedValue = (signed: string): string | null => {
14  const idx = signed.lastIndexOf('.');
15  if (idx === -1) return null;
16  const value = signed.slice(0, idx);
17  const sig = signed.slice(idx + 1);
18
19  const hmac = crypto.createHmac('sha256', SECRET);
20  hmac.update(value);
21  const expected = hmac.digest('hex');
22  // use timing-safe comparison in production
23  if (crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
24    return value;
25  }
26  return null;
27};
28
29// Example usage:
30const signed = signValue('userId=42');
31console.log('signed:', signed);
32console.log('verified:', verifySignedValue(signed)); // => "userId=42"
  • 透過此方法,您可以檢查 Cookie 值是否被篡改。不要將簽署金鑰包含在原始碼中;請透過環境變數等安全方式加以管理。

Cookie 與 CSRF 的緩解(一般模式)

使用 Cookie 的工作階段需要 CSRF 保護。常見的緩解措施包括:。

  • SameSite 設為 LaxStrict 以避免不必要的跨站傳送。僅在需要跨來源通訊時才使用 SameSite=None; Secure
  • 對於表單與 API 請求,使用由伺服器驗證的 CSRF 權杖。不要將權杖存放在 HttpOnly Cookie 中;應改為透過回應主體或 meta 標籤提供,並只在必要時由 JavaScript 讀取
  • 如果 JavaScript 可以存取該權杖,務必同時實作 XSS 防護(CSP、輸出跳脫等)。
  • 對敏感操作與登入要求重新驗證,並防止工作階段固定攻擊。

以下是一個安全的用戶端範例,會在標頭中傳送 CSRF 權杖。這是假設用戶端已從伺服器取得權杖,例如透過回應主體。

 1// Example of sending CSRF token safely in a header using fetch.
 2// Assumes CSRF token was provided securely (e.g., via response body or meta tag).
 3async function sendProtectedRequest(url: string, csrfToken: string) {
 4  const res = await fetch(url, {
 5    method: 'POST',
 6    credentials: 'same-origin', // include cookies for same-site requests
 7    headers: {
 8      'Content-Type': 'application/json',
 9      'X-CSRF-Token': csrfToken
10    },
11    body: JSON.stringify({ action: 'doSomething' })
12  });
13  return res;
14}
  • 若指定 credentials: 'same-origin',則只會傳送相同來源的 Cookie。在伺服器端,驗證 X-CSRF-Token 標頭的值,並確認權杖是否相符。若必須進行跨站請求,請謹慎設定 CORS。

跨站(CORS)與 credentials 的關係

在跨來源通訊中要傳送與接收 Cookie,用戶端必須指定 credentials: 'include',且伺服器必須設定 Access-Control-Allow-Credentials: true。但請將此設定限制於受信任的來源,並且不要使用 Access-Control-Allow-Origin: *

 1async function fetchCrossOriginData() {
 2  // Example: cross-origin fetch sending cookies.
 3  // Server must set Access-Control-Allow-Credentials: true
 4  // and a specific trusted origin.
 5  const res = await fetch('https://api.example.com/data', {
 6    credentials: 'include', // send cookies only to trusted domains
 7    method: 'GET'
 8  });
 9  return res;
10}
  • 從安全性角度來看,CORS 與 Cookie 的組合非常敏感。以允許清單的方式嚴格管理可允許的來源,並避免不必要的跨站通訊。此外,使用 SameSite=None; Secure 時,務必強制使用 HTTPS 以防止中間人攻擊。

範例:使用 Express 的安全工作階段 Cookie 設定(TypeScript)

實務上可使用 Express、cookie 函式庫、express-session 等。以下是使用 expresscookie-parser 的簡單範例。實務上請將 secure 設為 true,並透過環境變數管理 secret

 1// Express example using cookie-parser and setting a secure httpOnly cookie.
 2// npm install express cookie-parser @types/express @types/cookie-parser
 3import express from 'express';
 4import cookieParser from 'cookie-parser';
 5
 6const app = express();
 7app.use(cookieParser(process.env.COOKIE_SECRET));
 8
 9app.post('/login', (req, res) => {
10  // authenticate user (omitted)
11  const sessionId = 'generated-session-id';
12  res.cookie('sid', sessionId, {
13    httpOnly: true,
14    secure: true,        // require HTTPS in production
15    sameSite: 'lax',
16    maxAge: 1000 * 60 * 60 // 1 hour
17  });
18  res.json({ ok: true });
19});
20
21app.listen(3000);
  • 使用 cookieParsersecret 功能,便可輕鬆處理已簽章的 Cookie。然而,在實際應用中,基於安全性與可擴充性考量,不應直接將資料存放於 Cookie;而應使用專用的工作階段儲存庫(session store)。

Cookie 前綴 __Host-__Secure-

瀏覽器會對特定前綴強制套用特殊規則。

  • __Secure- 前綴 若 Cookie 名稱以 __Secure- 開頭,必須帶有 Secure 屬性。
  • __Host- 前綴 若以 __Host- 開頭,必須為 Secure、路徑須為 /(根目錄),且不可設定 Domain

使用這些可降低錯誤設定並提升安全性。

Cookie 的最佳實務

為了安全地處理 Cookie,請注意以下幾點。

  • 不要在 Cookie 中直接儲存敏感資訊。對於存取權杖,優先使用伺服器端的工作階段 ID。
  • 為工作階段 Cookie 設定 HttpOnlySecureSameSite=Lax(或 Strict)。
  • 善用 __Host-__Secure- 等前綴。
  • 考慮使用簽章(HMAC)與加密以防止竄改與竊聽。
  • 啟用 Secure 並強制使用 HTTPS。
  • 採取最小權限並設定短效期。
  • 使用 CSRF 權杖。
  • 留意不同瀏覽器(尤其舊版)在 SameSite 行為上的差異。

關於 Cookie 的常見誤解

關於 Cookie,請留意以下常見的誤解。

  • '加入 HttpOnly 就能消除 XSS 的影響。' 雖然 HttpOnly 可防止 JavaScript 存取 Cookie,但 XSS 仍可被用來發出任意請求。也應採用 CSRF 權杖驗證、CSP(內容安全政策)以及輸入清理。
  • '本機開發不需要 Secure。' 即使在本機環境中也模擬 HTTPS 並驗證行為,可提升測試的準確性。至少在預備(staging)與正式(production)環境中應使用 HTTPS。
  • '較長的到期時間很方便。' 若將 Cookie 的到期時間設得很長,一旦被竊取,能被濫用的期間也會隨之增加。可設定較短的到期時間,並加入定期重新驗證與權杖輪替。

總結

Cookie 雖然容易使用,但處理不當會引入安全性弱點。要用 TypeScript 正確管理 Cookie,重要的是理解 HttpOnlySecureSameSite 等屬性,並強制採用安全的伺服器端設定。透過不直接儲存敏感資料,並結合簽章與短效期設定,即可達成安全且可靠的工作階段管理。

您可以在我們的 YouTube 頻道上使用 Visual Studio Code 來跟隨上述文章一起學習。 請也查看我們的 YouTube 頻道。

YouTube Video