本文 AI 產出,尚未審核

ExpressJS (TypeScript)

單元:Request 與 Response 操作

主題:Cookie 與 Session 管理


簡介

在 Web 應用程式中,使用者的狀態管理是最常見也是最重要的需求之一。
無論是實作登入驗證、購物車、或是個人化設定,都少不了 CookieSession 的配合。

在 Express 搭配 TypeScript 的開發環境裡,正確且安全地操作這兩者,不只可以提升開發效率,還能避免許多安全漏洞(如 CSRF、XSS)。本篇文章將從概念說明、實作範例、常見陷阱到最佳實踐,完整帶你掌握 Cookie 與 Session 的使用方式,讓你的 Express 應用更加穩定、可維護。


核心概念

1️⃣ Cookie 基礎

  • Cookie 是由瀏覽器保存的小型資料片段,會在每次 HTTP 請求時自動帶回伺服器。
  • 它的屬性包括 name, value, maxAge, httpOnly, secure, sameSite 等,這些屬性決定了資料的存活時間與安全性。

注意:Cookie 的大小上限約為 4KB,且會被全部傳回伺服器,過多的 Cookie 會增加請求負擔。

2️⃣ 設定與讀取 Cookie

在 Express 中,我們通常使用 cookie-parser 中介軟體來簡化 Cookie 的操作。以下範例示範 設定、讀取、以及簽名 (signed) Cookie 的完整流程。

// src/app.ts
import express, { Request, Response, NextFunction } from 'express';
import cookieParser from 'cookie-parser';

const app = express();

// 使用 cookie-parser,傳入簽名密鑰(可選)
app.use(cookieParser('mySuperSecretKey'));

app.get('/set-cookie', (req: Request, res: Response) => {
  // 設定普通 Cookie
  res.cookie('theme', 'dark', {
    maxAge: 1000 * 60 * 60 * 24 * 30, // 30 天
    httpOnly: true,                    // 前端無法透過 JS 讀取
    sameSite: 'strict',                // 防止 CSRF
  });

  // 設定簽名 Cookie(安全性較高)
  res.cookie('userId', '12345', {
    signed: true,
    maxAge: 1000 * 60 * 60, // 1 小時
  });

  res.send('Cookie 已設定');
});

app.get('/read-cookie', (req: Request, res: Response) => {
  // 讀取普通 Cookie
  const theme = req.cookies.theme;
  // 讀取簽名 Cookie,需要使用 signedCookies
  const userId = req.signedCookies.userId;

  res.json({ theme, userId });
});

app.listen(3000, () => console.log('Server running on http://localhost:3000'));

說明

  • res.cookie(name, value, options) 用來 設定 Cookie。
  • req.cookies 取得未簽名的 Cookie,req.signedCookies 取得已簽名的 Cookie。
  • httpOnly: true 可防止前端 JavaScript 直接存取,降低 XSS 攻擊風險。
  • sameSite: 'strict' | 'lax' | 'none' 用於防止 CSRF,若要在跨站請求中傳送,必須配合 secure: true

3️⃣ Session 基礎

  • Session 是伺服器端保存的使用者狀態,通常會以唯一的 Session ID 透過 Cookie 回傳給瀏覽器。
  • 與 Cookie 不同,Session 的資料儲存在伺服器(記憶體、Redis、MongoDB…),因此可以保存較大或較敏感的資訊。

關鍵概念:Session 本身不會直接暴露資料給用戶端,只會傳遞一個 Session ID(通常是 connect.sid),這個 ID 再對應到伺服器端的資料。

4️⃣ Express Session 與 TypeScript

express-session 是最常用的 Session 中介軟體,搭配 TypeScript 時需要擴充 express-session 的型別,以便在 req.session 上取得自訂屬性。

// src/types/express-session.d.ts
import 'express-session';

declare module 'express-session' {
  interface SessionData {
    /** 用於儲存登入後的使用者資訊 */
    user?: {
      id: string;
      name: string;
      role: 'admin' | 'user';
    };
    /** 任意自訂屬性皆可在此聲明 */
    cart?: string[];
  }
}

設定 Session 中介軟體

// src/app.ts
import express, { Request, Response } from 'express';
import session from 'express-session';
import connectMongo from 'connect-mongo';
import mongoose from 'mongoose';

const app = express();

// 1. 連接 MongoDB(作為 Session Store,避免記憶體重啟遺失)
mongoose.connect('mongodb://localhost:27017/session-demo', {
  useNewUrlParser: true,
  useUnifiedTopology: true,
});

const MongoStore = connectMongo(session);

// 2. 設定 express-session
app.use(
  session({
    name: 'sid',                     // 自訂 Cookie 名稱,預設是 connect.sid
    secret: 'anotherSuperSecretKey', // 用於簽名 Session ID
    resave: false,                   // 不在每次請求時都重新儲存
    saveUninitialized: false,        // 未初始化的 session 不儲存
    store: new MongoStore({ mongooseConnection: mongoose.connection }),
    cookie: {
      maxAge: 1000 * 60 * 60 * 2, // 2 小時
      httpOnly: true,
      sameSite: 'lax',
      secure: process.env.NODE_ENV === 'production', // 僅在 HTTPS 時傳送
    },
  })
);

使用 Session

// src/routes/auth.ts
import { Router, Request, Response } from 'express';
const router = Router();

// 假設有一個簡易的驗證函式
function validateUser(username: string, password: string) {
  // 這裡僅示範,實務上請使用資料庫與加密驗證
  return username === 'admin' && password === '123456'
    ? { id: '1', name: 'Admin', role: 'admin' }
    : null;
}

// 登入
router.post('/login', (req: Request, res: Response) => {
  const { username, password } = req.body;
  const user = validateUser(username, password);
  if (!user) return res.status(401).json({ message: 'Invalid credentials' });

  // 將使用者資訊寫入 session
  req.session.user = user;
  res.json({ message: 'Login success' });
});

// 取得目前登入資訊
router.get('/me', (req: Request, res: Response) => {
  if (!req.session.user) return res.status(401).json({ message: 'Not logged in' });
  res.json({ user: req.session.user });
});

// 登出
router.post('/logout', (req: Request, res: Response) => {
  req.session.destroy((err) => {
    if (err) return res.status(500).json({ message: 'Logout failed' });
    // 清除 cookie
    res.clearCookie('sid');
    res.json({ message: 'Logged out' });
  });
});

export default router;

說明

  • req.session.user 為自訂屬性,透過前面的型別擴充,IDE 能提供自動完成與型別檢查。
  • saveUninitialized: false 避免產生空的 Session,減少資料庫寫入。
  • res.clearCookie('sid') 配合 req.session.destroy 完全移除使用者的登入狀態。

5️⃣ 結合 Cookie 與 Session

在實務上,我們常會 同時使用 Cookie(存放少量非敏感資訊)與 Session(存放較大或較敏感的資料)。以下示範一個 語系偏好 的實作:

// src/middleware/i18n.ts
import { Request, Response, NextFunction } from 'express';

export function i18nMiddleware(req: Request, res: Response, next: NextFunction) {
  // 1. 先從 Cookie 取得語系設定
  const localeFromCookie = req.cookies.locale;

  // 2. 若無 Cookie,檢查 Session 是否已有偏好
  const localeFromSession = req.session.locale;

  // 3. 預設語系
  const defaultLocale = 'zh-TW';

  // 4. 決定最終使用的語系
  const locale = localeFromCookie || localeFromSession || defaultLocale;

  // 5. 把結果掛在 res.locals,供之後的路由或模板使用
  res.locals.locale = locale;

  // 6. 若使用者剛登入且尚未有 Cookie,寫入 Cookie 以便下次直接讀取
  if (!localeFromCookie && localeFromSession) {
    res.cookie('locale', localeFromSession, {
      maxAge: 1000 * 60 * 60 * 24 * 365, // 1 年
      httpOnly: false, // 前端可讀取,用於 UI 切換
    });
  }

  next();
}

app.ts 中掛載中介軟體:

import { i18nMiddleware } from './middleware/i18n';
app.use(i18nMiddleware);

透過上述方式,我們可以 把 Session 中的偏好同步至 Cookie,讓前端在不需要再次向伺服器請求的情況下,就能直接讀取語系設定,提升使用者體驗。


常見陷阱與最佳實踐

陷阱 說明 最佳做法
Cookie 明文儲存敏感資訊 若把密碼或 token 直接寫入 Cookie,會被竊取。 使用 httpOnlysecure,或改用 Session 儲存敏感資料。
未設定 SameSite 跨站請求時,Cookie 仍會被送出,易受 CSRF 攻擊。 cookiesession.cookie 設定 sameSite: 'lax'(大多數情況)或 strict
Session Store 使用記憶體 伺服器重啟會遺失所有 Session,且不適合水平擴展。 使用 Redis、MongoDBMemcached 等外部儲存。
過度依賴 saveUninitialized: true 會為每個訪問者產生空的 Session,浪費資源。 設為 false,只在需要時才建立 Session。
忘記在 HTTPS 環境下啟用 secure Cookie 在 HTTP 中傳輸可能被竊聽。 process.env.NODE_ENV === 'production' 時,將 secure: true
未清除 Session 或 Cookie 使用者登出後仍能透過舊的 Session ID 取得資源。 在登出時 req.session.destroyres.clearCookie
型別未擴充 TypeScript 會把 req.session 看成 any,失去型別安全。 如前範例,使用 declare module 擴充 SessionData

實際應用場景

  1. 電子商務平台的購物車

    • 使用 Session 儲存購物車商品 ID(可能包含大量資料),避免暴露於前端。
    • 同時使用 Cookie 儲存「最後一次瀏覽的類別」或「語系」,讓 UI 快速呈現個人化內容。
  2. 單點登入 (SSO) 系統

    • 透過 Signed Cookie 保存一次性驗證 token,並在伺服器端以 Session 記錄使用者的權限資訊。
    • 這樣即使前端被 XSS 攻擊,簽名的 Cookie 仍難以偽造。
  3. 多語系網站

    • 如上面的 i18nMiddleware,結合 Cookie(讓前端直接讀取)與 Session(在登入後同步)提供一致的語系體驗。
  4. API 伺服器的防止 CSRF

    • 為所有需要驗證的 API 設定 SameSite: 'strict',並搭配 CSRF token(存於 Session),雙層保護。

總結

  • CookieSession 各有適用情境:
    • Cookie 輕量、無狀態,適合儲存偏好設定、臨時 token。
    • Session 有狀態、伺服器端,適合保存登入資訊、購物車等較大或較敏感的資料。
  • Express + TypeScript 中,使用 cookie-parserexpress-session 並配合 型別擴充,能讓開發者在 IDE 中獲得完整的型別提示,降低錯誤率。
  • 安全性 是不可妥協的要件:
    • 設定 httpOnlysecuresameSite
    • 使用 signed Cookie 防止被竄改。
    • 避免把敏感資訊直接寫入 Cookie。
  • 最佳實踐 包括:使用外部 Session Store、適當的 saveUninitializedresave 設定、登出時清除 Session 與 Cookie、以及在 TypeScript 中正確擴充型別。

掌握了上述概念與實作技巧,你就能在 ExpressJS 專案裡建立 安全、可擴充且易維護 的使用者狀態管理機制。祝開發順利,寫出更好的 Web 應用!