iT邦幫忙

2025 iThome 鐵人賽

DAY 21
0
Modern Web

現在就學Node.js系列 第 21

使用者密碼安全 — bcrypt 與登入驗證 - Day21

  • 分享至 

  • xImage
  •  

在開發登入系統時,最致命的錯誤之一,就是把使用者密碼「明碼」存進資料庫。
一旦資料外洩,使用者的帳號、銀行,甚至其他平台的登入都可能被竊用。

舉個例子:

{
  "name": "Alice",
  "email": "alice@example.com",
  "password": "123456" 
}

即使資料庫本身有加密,明碼密碼仍可能:

  • 被程式錯誤回傳給前端
  • 被中間人攔截

因此,密碼應該在儲存前就被單向加密(Hash)

🔐bcrypt:安全的密碼加密方案

bcrypt 是目前業界最普遍使用的密碼雜湊演算法之一,
它的設計初衷就是「抗暴力破解」與「確保唯一性」。
它有三個關鍵特點:

1. 不可逆(單向加密)

bcrypt 使用雜湊函數將密碼轉換成固定長度的字串,這個過程是單向的,無法從結果反推回原始密碼。

原始密碼: "password123"
加密結果: "$2b$10$U3TphxY7kQj5K8vN9..."

即便是同一個密碼組,每次加密的結果都不同。

2. 自動加鹽(Salt)

Salt 是隨機產生的額外字串,與密碼混合後再進行雜湊。這確保了:

  • 相同密碼產生不同結果:Alice 和 Bob 都用 "123456",但儲存在資料庫中的雜湊值完全不同。
  • 防止彩虹表攻擊:駭客無法使用預先計算好的常見密碼雜湊表來破解。

使用者 A: "123456" + "隨機Salt_abc" → 雜湊值 X
使用者 B: "123456" + "隨機Salt_xyz" → 雜湊值 Y

3. 可調整的運算成本(Cost Factor)

bcrypt 允許設定「加密輪數」,也稱為 cost factor 。
數字越大,安全性越高,破解需要更多時間,但伺服器運算時間也會增加

安裝與環境準備

npm install bcrypt mongoose express

bcrypt套件為 密碼加密核心套件

範例實作:註冊與登入 API

基本設定

import express from "express";
import mongoose from "mongoose";
import bcrypt from "bcrypt";

const app = express();
app.use(express.json());

// 連線 MongoDB
await mongoose.connect("mongodb://localhost:27017/auth_demo");
console.log("✅ 已連接 MongoDB");

建立使用者 Schema

我們建立一個 User 模型,並在儲存前自動加密密碼。

const userSchema = new mongoose.Schema({
  name: { type: String, required: true, minlength: 2 },
  email: { type: String, required: true, unique: true, match: /.+\@.+\..+/ },
  password: { type: String, required: true, minlength: 6 }
});

// Hooks:儲存前自動加密密碼
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);

  • bcrypt.hash(密碼, saltRounds):第二個參數越大越安全,但會增加運算時間。
    一般建議值:10~12

註冊 API

註冊時,只要建立新使用者,Hook 會自動幫你加密密碼。

app.post("/register", async (req, res, next) => {
  try {
    const { name, email, password } = req.body;
    
    // 檢查 email 是否已被註冊
    const existingUser = await User.findOne({ email });
    if (existingUser) {
      return res.status(409).json({ error: "此 email 已被註冊" });
    }
    
    // 建立新使用者(密碼會自動加密)
    const user = await User.create({ name, email, password });
    
    // 回傳時移除密碼欄位
    const userResponse = user.toObject();
    delete userResponse.password;
    
    res.status(201).json({ 
      message: "註冊成功", 
      user: userResponse 
    });
  } catch (err) {
    next(err);
  }
});

登入 API — 使用 bcrypt.compare

登入時我們需要驗證密碼是否正確,步驟如下:

  1. 找出該使用者的資料。
  2. 使用 bcrypt.compare(輸入密碼, 資料庫密碼) 進行比對。
  3. 若比對成功 → 登入成功;否則回傳錯誤。
app.post("/login", async (req, res) => {
  try {
    const { email, password } = req.body;

    // 1. 找出該使用者
    const user = await User.findOne({ email });
    if (!user) {
      return res.status(404).json({ error: "找不到該帳號" });
    }

    // 2. 驗證密碼
    const isValid = await bcrypt.compare(password, user.password);
    if (!isValid) {
      return res.status(401).json({ error: "密碼錯誤" });
    }

    // 3. 登入成功
    const userResponse = user.toObject();
    delete userResponse.password;
    
    res.json({ 
      message: "登入成功", 
      user: userResponse 
    });
  } catch (err) {
    res.status(500).json({ error: "伺服器錯誤" });
  }
});

統一錯誤處理

app.use((err, req, res, next) => {
  console.error(err); // 記錄錯誤
  
  // Mongoose 驗證錯誤
  if (err.name === "ValidationError") {
    return res.status(400).json({ 
      error: "資料驗證失敗",
      details: err.message 
    });
  }
  
  // MongoDB 重複鍵錯誤(例如:重複的 email)
  if (err.code === 11000) {
    return res.status(409).json({ 
      error: "該 email 已被註冊" 
    });
  }
  
  // 其他未預期的錯誤
  res.status(500).json({ error: "伺服器錯誤" });
});

app.listen(3000, () =>
  console.log("🚀 伺服器運行中: http://localhost:3000")
);

使用 Postman 測試

註冊帳號

POST http://localhost:3000/register

{
  "name": "Alice",
  "email": "alice@example.com",
  "password": "password123"
}

✅ 回傳結果(密碼已被加密):

{
  "message": "註冊成功",
  "user": {
    "_id": "6703e4d5a1b2c3d4e5f67890",
    "name": "Alice",
    "email": "alice@example.com",
  }
}

登入帳號

POST http://localhost:3000/login

{
  "email": "alice@example.com",
  "password": "password123"
}

✅ 成功時回傳:

{
  "message": "登入成功",
  "user": {
    "name": "Alice",
    "email": "alice@example.com"
  }
}

密碼錯誤

使用錯誤密碼登入:
Request Body

{
  "email": "alice@example.com",
  "password": "wrongpassword"
}

❌ 錯誤回應:

{
  "error": "密碼錯誤"
}

小結

今天的學習讓我們理解密碼安全的關鍵技術和實作方法

bcrypt.hash() - 密碼加密

將使用者的明碼密碼透過複雜的雜湊演算法轉換成不可逆的加密字串。這個過程會自動加入隨機的 Salt,並經過多輪運算,最終安全地儲存到資料庫中。

bcrypt.compare() - 密碼驗證

負責登入時的密碼驗證工作。當使用者輸入密碼嘗試登入時,這個函數會將輸入的明碼與資料庫中儲存的雜湊值進行比對,判斷密碼是否正確。

Mongoose Hooks - 自動化加密

使用 pre("save") 中介函式,在儲存前自動執行密碼加密邏輯,避免重複程式碼。

Salt(加鹽) - 唯一性保證

為每個密碼添加一段隨機產生的字串後再進行雜湊,確保即使兩位使用者使用完全相同的密碼,儲存在資料庫中的雜湊值也會完全不同。有效防止彩虹表攻擊。

Cost Factor - 安全強度控制

調整加密的安全強度。數值越高,雜湊運算的輪數越多,破解難度越大,但相對地伺服器運算時間也會增加。目前業界建議值為 10~12


上一篇
Mongoose 驗證、Hooks、關聯 (Population) -Day 20
下一篇
JWT 登入與驗證 — 打造安全的 RESTful API -Day22
系列文
現在就學Node.js24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言