在開發登入系統時,最致命的錯誤之一,就是把使用者密碼「明碼」存進資料庫。
一旦資料外洩,使用者的帳號、銀行,甚至其他平台的登入都可能被竊用。
舉個例子:
{
"name": "Alice",
"email": "alice@example.com",
"password": "123456"
}
即使資料庫本身有加密,明碼密碼仍可能:
因此,密碼應該在儲存前就被單向加密(Hash)。
bcrypt
是目前業界最普遍使用的密碼雜湊演算法之一,
它的設計初衷就是「抗暴力破解」與「確保唯一性」。
它有三個關鍵特點:
bcrypt 使用雜湊函數將密碼轉換成固定長度的字串,這個過程是單向的,無法從結果反推回原始密碼。
原始密碼: "password123"
加密結果: "$2b$10$U3TphxY7kQj5K8vN9..."
即便是同一個密碼組,每次加密的結果都不同。
Salt 是隨機產生的額外字串,與密碼混合後再進行雜湊。這確保了:
使用者 A: "123456" + "隨機Salt_abc" → 雜湊值 X
使用者 B: "123456" + "隨機Salt_xyz" → 雜湊值 Y
bcrypt 允許設定「加密輪數」,也稱為 cost factor 。
數字越大,安全性越高,破解需要更多時間,但伺服器運算時間也會增加
npm install bcrypt mongoose express
bcrypt套件為 密碼加密核心套件
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");
我們建立一個 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)
:第二個參數越大越安全,但會增加運算時間。註冊時,只要建立新使用者,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);
}
});
bcrypt.compare
登入時我們需要驗證密碼是否正確,步驟如下:
bcrypt.compare(輸入密碼, 資料庫密碼)
進行比對。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")
);
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": "密碼錯誤"
}
今天的學習讓我們理解密碼安全的關鍵技術和實作方法
將使用者的明碼密碼透過複雜的雜湊演算法轉換成不可逆的加密字串。這個過程會自動加入隨機的 Salt,並經過多輪運算,最終安全地儲存到資料庫中。
負責登入時的密碼驗證工作。當使用者輸入密碼嘗試登入時,這個函數會將輸入的明碼與資料庫中儲存的雜湊值進行比對,判斷密碼是否正確。
使用 pre("save")
中介函式,在儲存前自動執行密碼加密邏輯,避免重複程式碼。
為每個密碼添加一段隨機產生的字串後再進行雜湊,確保即使兩位使用者使用完全相同的密碼,儲存在資料庫中的雜湊值也會完全不同。有效防止彩虹表攻擊。
調整加密的安全強度。數值越高,雜湊運算的輪數越多,破解難度越大,但相對地伺服器運算時間也會增加。目前業界建議值為 10~12。