ExpressJS (TypeScript)
單元:Request 與 Response 操作
主題:Cookie 與 Session 管理
簡介
在 Web 應用程式中,使用者的狀態管理是最常見也是最重要的需求之一。
無論是實作登入驗證、購物車、或是個人化設定,都少不了 Cookie 與 Session 的配合。
在 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,會被竊取。 | 使用 httpOnly、secure,或改用 Session 儲存敏感資料。 |
| 未設定 SameSite | 跨站請求時,Cookie 仍會被送出,易受 CSRF 攻擊。 | 在 cookie 或 session.cookie 設定 sameSite: 'lax'(大多數情況)或 strict。 |
| Session Store 使用記憶體 | 伺服器重啟會遺失所有 Session,且不適合水平擴展。 | 使用 Redis、MongoDB 或 Memcached 等外部儲存。 |
過度依賴 saveUninitialized: true |
會為每個訪問者產生空的 Session,浪費資源。 | 設為 false,只在需要時才建立 Session。 |
忘記在 HTTPS 環境下啟用 secure |
Cookie 在 HTTP 中傳輸可能被竊聽。 | 在 process.env.NODE_ENV === 'production' 時,將 secure: true。 |
| 未清除 Session 或 Cookie | 使用者登出後仍能透過舊的 Session ID 取得資源。 | 在登出時 req.session.destroy 並 res.clearCookie。 |
| 型別未擴充 | TypeScript 會把 req.session 看成 any,失去型別安全。 |
如前範例,使用 declare module 擴充 SessionData。 |
實際應用場景
電子商務平台的購物車
- 使用 Session 儲存購物車商品 ID(可能包含大量資料),避免暴露於前端。
- 同時使用 Cookie 儲存「最後一次瀏覽的類別」或「語系」,讓 UI 快速呈現個人化內容。
單點登入 (SSO) 系統
- 透過 Signed Cookie 保存一次性驗證 token,並在伺服器端以 Session 記錄使用者的權限資訊。
- 這樣即使前端被 XSS 攻擊,簽名的 Cookie 仍難以偽造。
多語系網站
- 如上面的
i18nMiddleware,結合 Cookie(讓前端直接讀取)與 Session(在登入後同步)提供一致的語系體驗。
- 如上面的
API 伺服器的防止 CSRF
- 為所有需要驗證的 API 設定 SameSite: 'strict',並搭配 CSRF token(存於 Session),雙層保護。
總結
- Cookie 與 Session 各有適用情境:
- Cookie 輕量、無狀態,適合儲存偏好設定、臨時 token。
- Session 有狀態、伺服器端,適合保存登入資訊、購物車等較大或較敏感的資料。
- 在 Express + TypeScript 中,使用
cookie-parser、express-session並配合 型別擴充,能讓開發者在 IDE 中獲得完整的型別提示,降低錯誤率。 - 安全性 是不可妥協的要件:
- 設定
httpOnly、secure、sameSite。 - 使用 signed Cookie 防止被竄改。
- 避免把敏感資訊直接寫入 Cookie。
- 設定
- 最佳實踐 包括:使用外部 Session Store、適當的
saveUninitialized與resave設定、登出時清除 Session 與 Cookie、以及在 TypeScript 中正確擴充型別。
掌握了上述概念與實作技巧,你就能在 ExpressJS 專案裡建立 安全、可擴充且易維護 的使用者狀態管理機制。祝開發順利,寫出更好的 Web 應用!