在前一章,我們學會了如何使用 bcrypt 來安全儲存使用者密碼。
但登入後,伺服器還需要知道「這個請求是哪位使用者發的?」
例如:
傳統做法是使用 Session + Cookie 來記錄登入狀態。
但在現代的 前後端分離架構(SPA、行動 App、API) 中,Session 管理不再方便,因此誕生了更彈性與通用的方案 —— JWT(JSON Web Token)。
JWT 是一種「可攜帶、可驗證、不可竄改」的數位令牌(Token)。
當使用者登入成功後,伺服器會簽發一個 Token 給前端。
前端保存這個 Token(通常放在 LocalStorage 或 Cookie),
並在每次呼叫 API 時一併附上,以便伺服器能驗證身份。
JWT 由三部分組成,中間以 .
分隔:
Header.Payload.Signature
描述 Token 類型與簽章演算法,例如:
{ "alg": "HS256", "typ": "JWT" }
保存主要內容(Claims),例如使用者資訊或 Token 狀態:
{
"id": "6703e4d5a1b2c3d4",
"name": "Alice",
"email": "alice@example.com",
"iat": 1696406400,
"exp": 1696410000
}
⚠️ 注意:Payload 是可解碼的,不要放敏感資料(如密碼或金鑰)。
伺服器用密鑰 (secret key) 對前兩段內容進行簽章,確保內容未被竄改。
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
Header.Payload
Signature
Header.Payload.Signature
每次呼叫 API 時,於 Header 中附上:
Authorization: Bearer <token>
即使駭客修改 payload(例如把 name: "Alice"
改成 Bob
),
他依然無法生成正確的 Signature,因為他沒有伺服器的 secret key。
伺服器在驗證時會發現簽章不符,請求立即被拒。
安全措施 | 說明 |
---|---|
不要放敏感資訊 | JWT Payload 可被解碼,請勿存密碼或金鑰 |
secret 保密 | 簽章密鑰請存於 .env 或安全環境中 |
加上過期時間 | 使用 exp 屬性限制存活時間(如 1 小時) |
使用 HTTPS | 防止 Token 被攔截或竊取 |
Refresh Token 機制 | 可重新簽發 Token,避免重登 |
npm install express mongoose bcrypt jsonwebtoken
import express from "express";
import mongoose from "mongoose";
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
const app = express();
app.use(express.json());
await mongoose.connect("mongodb://localhost:27017/jwt_demo");
console.log("✅ 已連接 MongoDB");
const userSchema = new mongoose.Schema({
name: { type: String, required: true, minlength: 2 },
email: { type: String, required: true, unique: true },
password: { type: String, required: true, minlength: 6 },
});
// 在儲存前自動加密密碼
userSchema.pre("save", async function (next) {
if (this.isModified("password")) {
this.password = await bcrypt.hash(this.password, 10);
}
next();
});
const User = mongoose.model("User", userSchema);
app.post("/register", async (req, res, next) => {
try {
const { name, email, password } = req.body;
const exist = await User.findOne({ email });
if (exist) return res.status(400).json({ error: "該信箱已註冊" });
const user = await User.create({ name, email, password });
res.status(201).json({ message: "註冊成功", user });
} catch (err) {
next(err);
}
});
app.post("/login", async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user) return res.status(404).json({ error: "找不到該帳號" });
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) return res.status(401).json({ error: "密碼錯誤" });
const token = jwt.sign(
{ id: user._id, email: user.email },
"mysecretkey", // ⚠️ 請使用環境變數
{ expiresIn: "1h" }
);
res.json({ message: "登入成功", token });
});
function verifyToken(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader) return res.status(401).json({ error: "缺少授權標頭" });
const token = authHeader.split(" ")[1];
jwt.verify(token, "mysecretkey", (err, decoded) => {
if (err) return res.status(403).json({ error: "Token 無效或過期" });
req.user = decoded;
next();
});
}
app.get("/profile", verifyToken, async (req, res) => {
const user = await User.findById(req.user.id).select("-password");
res.json({ message: "成功取得使用者資料", user });
});
1️⃣ 註冊帳號
POST http://localhost:3000/register
{
"name": "Alice",
"email": "alice@example.com",
"password": "password123"
}
2️⃣ 登入 取得Token
POST http://localhost:3000/login
{
"email": "alice@example.com",
"password": "password123"
}
3️⃣ 驗證 Token
GET http://localhost:3000/profile
Header: Authorization: Bearer <token>
完成本章後,已經掌握了 JWT 驗證的完整實作流程。
讓我們回顧關鍵概念
登入階段:使用者提供帳號密碼 → 伺服器驗證 → 簽發 JWT Token
儲存階段:前端將 Token 儲存在 LocalStorage 或 SessionStorage
使用階段:每次 API 請求在 Header 加入 Authorization: Bearer
驗證階段:伺服器提取 Token → 驗證簽章 → 檢查過期時間 → 執行 API 邏輯
過期處理:Token 過期後,使用者需要重新登入(或使用 Refresh Token)