JWT 認證實戰教學:用 Node.js 打造安全的 API 登入系統
上個月幫一個新創團隊 code review,發現他們的 API 認證機制是自己「發明」的 — 把使用者 ID 直接 base64 編碼放在 header 裡。這不是認證,這是在裸奔。
如果你正在做 REST API 或 GraphQL 的後端開發,JWT 幾乎是你繞不開的認證方案。今天我們就來從零開始,用 Node.js 打造一個安全的 JWT 認證系統。
JWT 到底是什麼?
JWT(JSON Web Token)是一個開放標準(RFC 7519),用來在兩方之間安全地傳遞資訊。你可以把它想像成一張「電子通行證」— 伺服器簽發給你,之後你每次來都出示這張通行證,伺服器驗證簽名確認是自己發的,就讓你通過。
跟傳統的 Session 機制不同,JWT 是「無狀態」的。伺服器不需要在記憶體或資料庫裡記錄「誰已經登入」,所有需要的資訊都包含在 Token 本身裡面。
JWT vs Session:為什麼選 JWT?
先說結論:不是所有場景都適合 JWT,但在微服務架構和前後端分離的專案裡,JWT 有明顯優勢。
| 特性 | Session | JWT |
|---|---|---|
| 狀態 | 有狀態(伺服器端儲存) | 無狀態(Token 自帶資訊) |
| 擴展性 | 需要 Session Store(Redis等) | 天生支援水平擴展 |
| 跨域 | 需要特殊設定 | 原生支援 CORS |
| 行動端 | Cookie 處理麻煩 | 直接放在 Header |
| 登出 | 刪除 Session 即可 | 需要額外機制(黑名單等) |
如果你的架構用到 Redis 做快取,其實用 Session + Redis 也很好。JWT 的真正優勢在於微服務之間不需要共享 Session Store。
JWT 的三段式結構
一個 JWT Token 長得像這樣:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjMiLCJyb2xlIjoiYWRtaW4ifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
用 . 分成三段:
- Header:演算法和 Token 類型(
{"alg":"HS256","typ":"JWT"}) - Payload:載荷資料,例如使用者 ID、角色、過期時間
- Signature:用 secret key 對前兩段做簽名,防止竄改
重要觀念:JWT 不是加密,是簽名。Payload 只是 base64 編碼,任何人都能解讀內容。千萬不要把密碼、信用卡號等敏感資料放在 Payload 裡。
實作登入與 Token 簽發
先安裝必要套件:
npm install express jsonwebtoken bcryptjs dotenv
然後來寫登入邏輯:
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
// .env 裡面設定 JWT_SECRET=你的超長隨機字串
const JWT_SECRET = process.env.JWT_SECRET;
const JWT_EXPIRES_IN = '15m'; // Access Token 只活 15 分鐘
app.post('/api/auth/login', async (req, res) => {
const { email, password } = req.body;
// 1. 從資料庫找使用者
const user = await db.users.findByEmail(email);
if (!user) {
return res.status(401).json({ error: '帳號或密碼錯誤' });
// 不要告訴攻擊者是「帳號不存在」還是「密碼錯誤」
}
// 2. 驗證密碼
const isValid = await bcrypt.compare(password, user.passwordHash);
if (!isValid) {
return res.status(401).json({ error: '帳號或密碼錯誤' });
}
// 3. 簽發 Token
const accessToken = jwt.sign(
{ userId: user.id, role: user.role },
JWT_SECRET,
{ expiresIn: JWT_EXPIRES_IN }
);
res.json({ accessToken, user: { id: user.id, name: user.name } });
});
注意那個錯誤訊息 — 不管是帳號不存在還是密碼錯誤,都回傳一樣的訊息。這是為了防止攻擊者透過錯誤訊息來枚舉有效帳號。
撰寫認證中間件
有了 Token,接下來要寫一個 middleware 來驗證每個受保護的 API 請求:
function authMiddleware(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: '未提供認證 Token' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, JWT_SECRET);
req.user = decoded; // 把使用者資訊掛在 request 上
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token 已過期' });
}
return res.status(403).json({ error: '無效的 Token' });
}
}
// 使用方式
app.get('/api/profile', authMiddleware, (req, res) => {
// req.user.userId 和 req.user.role 可以直接用
res.json({ userId: req.user.userId });
});
Refresh Token 機制
Access Token 設定 15 分鐘過期是業界慣例,但使用者不會想每 15 分鐘重新登入一次。這就是 Refresh Token 的用途。
核心概念:
- Access Token:短命(15 分鐘),用來存取 API
- Refresh Token:長命(7-30 天),用來換新的 Access Token
// 登入時同時簽發 Refresh Token
const refreshToken = jwt.sign(
{ userId: user.id },
REFRESH_SECRET,
{ expiresIn: '7d' }
);
// 把 Refresh Token 存到 HttpOnly Cookie
res.cookie('refreshToken', refreshToken, {
httpOnly: true, // JavaScript 無法讀取
secure: true, // 只透過 HTTPS 傳送
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000
});
// 換發 Access Token 的 endpoint
app.post('/api/auth/refresh', (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) return res.status(401).json({ error: '未提供 Refresh Token' });
try {
const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
const newAccessToken = jwt.sign(
{ userId: decoded.userId, role: decoded.role },
JWT_SECRET,
{ expiresIn: '15m' }
);
res.json({ accessToken: newAccessToken });
} catch (err) {
res.status(403).json({ error: '無效的 Refresh Token' });
}
});
安全性最佳實踐
- 永遠用 HTTPS:Token 在 HTTP 下會被中間人攻擊截獲
- Secret Key 要夠長:至少 256 bits(32 字元以上的隨機字串)
- 設定合理的過期時間:Access Token 15-30 分鐘,Refresh Token 7-30 天
- Refresh Token 存在 HttpOnly Cookie:防止 XSS 竊取
- 實作 Token Rotation:每次使用 Refresh Token 時,同時簽發新的 Refresh Token 並作廢舊的
- 不要在 Payload 放敏感資料:只放 userId 和 role 就好
如果你的後端還用到 Docker 部署,記得在環境變數裡設定 Secret Key,不要寫死在程式碼裡。
常見安全漏洞與防範
漏洞 1:Algorithm None Attack
攻擊者把 header 的 alg 改成 "none",繞過簽名驗證。防範方式是在 verify 時明確指定演算法:
jwt.verify(token, SECRET, { algorithms: ['HS256'] });
漏洞 2:Weak Secret Key
如果 secret key 太短或太常見(像是 "secret"、"password"),攻擊者可以用暴力破解的方式猜出來。
漏洞 3:Token 存在 localStorage
XSS 攻擊可以讀取 localStorage 裡的 Token。Access Token 放在記憶體中(JavaScript 變數),Refresh Token 放在 HttpOnly Cookie 中是比較安全的做法。
漏洞 4:沒有實作 Token 黑名單
JWT 無法真正「登出」,因為 Token 在過期前一直有效。如果使用者修改密碼或被停權,你需要一個 Token 黑名單機制。可以用 Redis 來存被作廢的 Token。
總結
JWT 認證看似簡單,但細節裡藏著很多安全問題。記住這些原則:Access Token 要短命、Refresh Token 要存在 HttpOnly Cookie、Secret Key 要夠長、永遠在伺服器端驗證。
如果你還在猶豫要用 REST 還是 GraphQL 來設計你的 API,可以先看看我們的 GraphQL vs REST 完整比較,兩種架構都可以搭配 JWT 使用。
繼續閱讀
REST API 版本控制策略完整教學:URI Path、Header、Query 三種方案實戰比較與最佳實踐
相關文章
你可能也喜歡
探索其他領域的精選好文