iT邦幫忙

2025 iThome 鐵人賽

DAY 22
0
Modern Web

現在就學Node.js系列 第 22

JWT 登入與驗證 — 打造安全的 RESTful API -Day22

  • 分享至 

  • xImage
  •  

在前一章,我們學會了如何使用 bcrypt 來安全儲存使用者密碼。

但登入後,伺服器還需要知道「這個請求是哪位使用者發的?」

例如:

  • 使用者登入後要能查看自己的個人資料
  • 不同角色(如管理員、一般使用者)擁有不同權限
  • 防止未授權的使用者竄改或假冒他人身份

傳統做法是使用 Session + Cookie 來記錄登入狀態。

但在現代的 前後端分離架構(SPA、行動 App、API) 中,Session 管理不再方便,因此誕生了更彈性與通用的方案 —— JWT(JSON Web Token)

🔑 JWT 是什麼?

JWT 是一種「可攜帶、可驗證、不可竄改」的數位令牌(Token)。

當使用者登入成功後,伺服器會簽發一個 Token 給前端。

前端保存這個 Token(通常放在 LocalStorageCookie),

並在每次呼叫 API 時一併附上,以便伺服器能驗證身份。

特點

  • 無狀態 (Stateless):伺服器不需保存登入狀態。
  • 可跨服務使用:JWT 可在多台伺服器或不同系統間共用。
  • 自帶驗證機制:Token 內含使用者資訊與簽章,能防止竄改。

JWT 結構

JWT 由三部分組成,中間以 . 分隔:

Header.Payload.Signature

1️⃣ Header

描述 Token 類型與簽章演算法,例如:

{ "alg": "HS256", "typ": "JWT" }

2️⃣ Payload

保存主要內容(Claims),例如使用者資訊或 Token 狀態:

{
  "id": "6703e4d5a1b2c3d4",
  "name": "Alice",
  "email": "alice@example.com",
  "iat": 1696406400,
  "exp": 1696410000
}

⚠️ 注意:Payload 是可解碼的,不要放敏感資料(如密碼或金鑰)。

3️⃣ Signature

伺服器用密鑰 (secret key) 對前兩段內容進行簽章,確保內容未被竄改。

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

🔐 JWT 驗證流程

簽發 Token 時

  1. 伺服器準備 Header + Payload
  2. 對兩者進行 Base64Url 編碼 → Header.Payload
  3. 使用伺服器的 secret 簽章 → 產生 Signature
  4. 組合成完整 Token → Header.Payload.Signature

客戶端請求時

每次呼叫 API 時,於 Header 中附上:

Authorization: Bearer <token>

伺服器驗證時

  1. 伺服器解析收到的 Token → 拆出 Header / Payload / Signature
  2. 使用同樣的 secret 重新簽章
  3. 比對簽章結果:
    • ✅ 相同 → Token 有效
    • ❌ 不同 → Token 被竄改或偽造

⚠️ 為什麼駭客無法偽造 Token?

即使駭客修改 payload(例如把 name: "Alice" 改成 Bob),

他依然無法生成正確的 Signature,因為他沒有伺服器的 secret key

伺服器在驗證時會發現簽章不符,請求立即被拒。

安全性提醒

安全措施 說明
不要放敏感資訊 JWT Payload 可被解碼,請勿存密碼或金鑰
secret 保密 簽章密鑰請存於 .env 或安全環境中
加上過期時間 使用 exp 屬性限制存活時間(如 1 小時)
使用 HTTPS 防止 Token 被攔截或竊取
Refresh Token 機制 可重新簽發 Token,避免重登

實作:JWT 登入與驗證 API

Step 1️⃣ 安裝套件

npm install express mongoose bcrypt jsonwebtoken

Step 2️⃣ 建立伺服器與使用者模型

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);

Step 3️⃣ 註冊與登入 API

註冊 API

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);
  }
});

登入 API

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 });
});

Step 4️⃣ 建立 Middleware 驗證 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();
  });
}

Step 5️⃣ 受保護的 API

app.get("/profile", verifyToken, async (req, res) => {
  const user = await User.findById(req.user.id).select("-password");
  res.json({ message: "成功取得使用者資料", user });
});

Step 6️⃣ Postman 測試流程

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 驗證的完整實作流程。

讓我們回顧關鍵概念

  • JWT 是由三個部分組成的 Token:Header.Payload.Signature
  • Header 描述演算法和類型
  • Payload 包含使用者資料(不含敏感資訊)
  • Signature 是數位簽章,確保 Token 未被竄改
  • 整個 Token 是一個經過 Base64URL 編碼的字串

JWT的特點

  • 無狀態設計:伺服器不需要儲存 Session,減輕記憶體負擔
  • 易於擴展:多台伺服器都能獨立驗證 Token,無需共享 Session 資料
  • 跨域友善:適合前後端分離架構,Token 放在 HTTP Header 中傳送

完整認證流程

  1. 登入階段:使用者提供帳號密碼 → 伺服器驗證 → 簽發 JWT Token

  2. 儲存階段:前端將 Token 儲存在 LocalStorage 或 SessionStorage

  3. 使用階段:每次 API 請求在 Header 加入 Authorization: Bearer

  4. 驗證階段:伺服器提取 Token → 驗證簽章 → 檢查過期時間 → 執行 API 邏輯

  5. 過期處理:Token 過期後,使用者需要重新登入(或使用 Refresh Token)


上一篇
使用者密碼安全 — bcrypt 與登入驗證 - Day21
下一篇
Session vs JWT + Token 儲存安全 - Day23
系列文
現在就學Node.js24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言