在 Node.js 中使用 bcrypt 加密密碼的完整指南

作者: Calpa Liu
字數:3150
出版:2025 年 3 月 19 日
#bcrypt#Node.js#security#database#salt#hashing algorithm
在現代應用程序開發中,安全存儲用戶密碼至關重要。本報告將詳細介紹如何在 Node.js 中使用 bcrypt 進行密碼加密,探討為何 bcrypt 優於其他哈希方法,並提供密碼加密的最佳實踐。

bcrypt 簡介

bcrypt
bcrypt

bcrypt 是一種專為密碼哈希設計的密碼學函數,由 Niels Provos 和 David Mazières 於 1999 年基於 Blowfish 加密算法開發,並在 USENIX 大會上發表。它不僅僅是一種普通的哈希算法,而是專門為解決密碼存儲安全問題而設計的解決方案。

bcrypt 的工作原理

bcrypt 結合了密碼字符串、成本因子和鹽值(salt)來計算哈希值。其哈希過程包含以下步驟:

  1. 生成一個 16 字節的隨機鹽值
  2. 將密碼與鹽值結合
  3. 根據指定的成本因子(工作因子)進行多輪哈希運算
  4. 生成最終的 24 字節哈希值

最終輸出的 bcrypt 哈希字符串格式為:

$2<a/b/x/y>$[cost]$[22 character salt][31 character hash]

例如:$2a$12$/NkvJmm3v1ua1Mc5s82X5OvW0bMVsSy0oTQH4do3BN4Td14pAMTiW的構造為:

  • $2$(1999 年):原始 bcrypt 規範,遵循模塊化加密格式(Modular Crypt Format)。這是最初的版本,但缺乏對非 ASCII 字符的良好支持。

  • $2a$(2007 年):修訂規範,改進了 UTF-8 編碼處理和空終止符(null terminator)問題。這個版本大大提高了對國際字符的支持,使 bcrypt 更適合全球化應用。

  • $2x$$2y$(2011 年 6 月):

    • $2x$:由於 PHP 中 crypt_blowfish 實現的一個錯誤而引入。這個版本標記了可能存在漏洞的實現。
    • $2y$:PHP 的修復版本,解決了 $2x$ 中的問題,提供了更安全的實現。
  • $2b$(2014 年 2 月):解決了 OpenBSD 實現中的密碼長度錯誤。這個版本修復了一個重要的安全漏洞,即某些情況下密碼可能被截斷,從而降低安全性。$2b$ 確保了所有輸入的字符都被正確處理,是目前最推薦使用的版本。

每個版本的更新都旨在提高 bcrypt 的安全性和兼容性,反映了密碼學界對潛在漏洞的持續關注和快速響應。選擇最新的 $2b$ 版本可以確保最高級別的安全性和兼容性。

  • 12:成本因子(工作因子)。這是一個 2 的對數值,表示哈希函數執行的迭代次數。值越高,計算哈希所需的時間越長,安全性越高,但也會增加系統負載。通常在 10 到 14 之間選擇,具體取決於系統性能和安全需求。
  • /NkvJmm3v1ua1Mc5s82X5O:鹽值(22 字節,22 字符)。這是隨機生成的,確保每個密碼的哈希值都是唯一的,即使相同的密碼也會產生不同的哈希結果。
  • vW0bMVsSy0oTQH4do3BN4Td14pAMTiW:哈希值(31 字節,31 字符)。這是最終的哈希結果,是密碼的加密表示。

為何選擇 bcrypt 而非其他哈希方法

1. 抵抗暴力破解攻擊的設計

bcrypt 的一個主要優勢是其刻意 緩慢 的計算特性。與 SHA256 等快速哈希算法不同,bcrypt 設計為消耗更多計算資源並花費更長時間生成哈希值。這使得黑客進行暴力破解時必須投入更多時間和計算資源,從而有效增加了密碼被破解的難度。

2. 自動鹽值處理

bcrypt 自動為每個密碼生成並添加隨機鹽值,確保即使相同的密碼也會產生不同的哈希結果。這有效防止了以下攻擊:

  • 彩虹表攻擊:預先計算的密碼 - 哈希對照表攻擊
  • 字典攻擊:使用常見密碼清單進行的攻擊
  • 查表攻擊:利用預先計算的密碼哈希表進行的攻擊

由於每個哈希都包含唯一的鹽值,即使兩個用戶使用完全相同的密碼,其存儲的哈希值也會完全不同。這大大增加了破解的難度和成本。

3. 適應性與可調整性

bcrypt 的一個關鍵特性是其可調整的成本因子(cost factor)。這允許開發者根據應用需求和硬件能力調整哈希計算的複雜度:

  • 自適應性:隨著硬件性能提升,可以增加成本因子
  • 靈活性:可以根據不同的安全需求調整計算強度
  • 未來適應性:通過調整參數來應對未來的計算能力提升
  • 性能平衡:在安全性和響應時間之間找到最佳平衡點

這種可調整性使 bcrypt 成為一個「經得起時間考驗」的加密方案,能夠持續適應計算技術的發展。

4. bcrypt vs 其他哈希算法的比較

bcrypt 與其他常見哈希算法的主要差異:

特性bcryptSHA256MD5
計算速度慢速(設計如此)快速非常快速
內建鹽值
可調整性
輸出長度固定(60 字符)64 字符32 字符
主要用途密碼存儲數據完整性校驗不推薦使用
抗暴力破解極弱
記憶體需求

SHA256 和 MD5 雖然計算效率高,但正是這種高效性使其容易受到暴力破解和彩虹表攻擊,因此不適合用於密碼存儲。bcrypt 的慢速特性和內建安全機制使其成為密碼存儲的理想選擇。

在 Node.js 中實現 bcrypt

安裝 bcrypt

在 Node.js 項目中使用 bcrypt 的第一步是安裝 bcrypt 包:

npm install bcrypt
# 或使用 Yarn
yarn add bcrypt

引入 bcrypt 模塊

在您的代碼中引入 bcrypt:

// CommonJS
const bcrypt = require('bcrypt');

// 或使用 ES modules
import * as bcrypt from 'bcrypt';

使用 bcrypt 加密密碼

生成哈希密碼

bcrypt 提供了異步(Promise 和回調)和同步兩種方法來哈希密碼:

異步方法(Promise,最推薦):

async function hashPassword(password: string): Promise<string> {
  const saltRounds = 10; // 工作因子
  try {
    const hash = await bcrypt.hash(password, saltRounds);
    return hash;
  } catch (error) {
    console.error('Error hashing password:', error);
    throw error;
  }
}

// 使用示例
try {
  const hash = await hashPassword('userPassword');
  // 存儲 hash 到數據庫
  console.log('Hashed password:', hash);
} catch (error) {
  // 錯誤處理
}

異步方法(回調):

const saltRounds = 10;
const password = 'userPassword';

bcrypt.hash(password, saltRounds, (err, hash) => {
  if (err) {
    console.error('Error hashing password:', err);
    return;
  }
  // 存儲 hash 到數據庫
  console.log('Hashed password:', hash);
});

同步方法(不推薦在生產環境使用):

const saltRounds = 10;
const password = 'userPassword';

try {
  const hash = bcrypt.hashSync(password, saltRounds);
  // 存儲 hash 到數據庫
  console.log('Hashed password:', hash);
} catch (err) {
  console.error('Error hashing password:', err);
}

注意:在生產環境中,強烈建議使用異步方法(特別是 Promise 方式),因為同步方法會阻塞事件循環,影響應用性能。

驗證密碼

當用戶登錄時,需要驗證輸入的密碼是否匹配存儲的哈希。bcrypt 提供了多種驗證方法:

異步方法(Promise,推薦):

async function verifyPassword(password: string, hash: string): Promise<boolean> {
  try {
    const isMatch = await bcrypt.compare(password, hash);
    return isMatch;
  } catch (error) {
    console.error('Error verifying password:', error);
    throw error;
  }
}

// 使用示例
try {
  const isValid = await verifyPassword('userPassword', storedHash);
  if (isValid) {
    console.log('Password is correct');
    // 處理登錄成功邏輯
  } else {
    console.log('Password is incorrect');
    // 處理登錄失敗邏輯
  }
} catch (error) {
  // 處理錯誤
  console.error('Password verification failed:', error);
}

異步方法(回調):

bcrypt.compare(password, hash, (err, result) => {
  if (err) {
    console.error('Error verifying password:', err);
    return;
  }
  
  if (result) {
    console.log('Password is correct');
    // 處理登錄成功邏輯
  } else {
    console.log('Password is incorrect');
    // 處理登錄失敗邏輯
  }
});

同步方法(不推薦):

try {
  const isMatch = bcrypt.compareSync(password, hash);
  if (isMatch) {
    console.log('Password is correct');
    // 處理登錄成功邏輯
  } else {
    console.log('Password is incorrect');
    // 處理登錄失敗邏輯
  }
} catch (error) {
  console.error('Error verifying password:', error);
}

安全提示

  1. 永遠使用時間恆定的比較方法(bcrypt.compare)
  2. 避免在錯誤消息中洩露具體的驗證失敗原因
  3. 考慮實施登錄嘗試次數限制以防止暴力破解

實際應用範例:Express 應用中的用戶認證

Express 應用中的用戶認證
Express 應用中的用戶認證

以下是一個使用 Express 框架和 bcrypt 實現用戶註冊與登錄的完整範例,包含了安全最佳實踐:

import express, { Request, Response } from 'express';
import * as bcrypt from 'bcrypt';
import rateLimit from 'express-rate-limit';

interface User {
  username: string;
  password: string;
}

const app = express();

// 安全中間件配置
app.use(express.json({ limit: '10kb' })); // 限制請求體大小

// 配置速率限制器
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 分鐘窗口
  max: 5, // 限制每個 IP 15 分鐘內最多 5 次嘗試
  message: '登錄嘗試次數過多,請稍後再試'
});

// 用戶註冊路由
app.post('/register', async (req: Request, res: Response) => {
  try {
    const { username, password }: User = req.body;

    // 輸入驗證
    if (!username || !password) {
      return res.status(400).json({
        status: 'error',
        message: '用戶名和密碼都是必需的'
      });
    }

    // 密碼強度檢查
    if (password.length < 8) {
      return res.status(400).json({
        status: 'error',
        message: '密碼必須至少包含 8 個字符'
      });
    }

    const saltRounds = 10;
    const hash = await bcrypt.hash(password, saltRounds);

    // 將用戶數據保存到數據庫
    // await db.saveUser({
    //   username,
    //   password: hash,
    //   createdAt: new Date(),
    //   lastLogin: null
    // });

    res.status(201).json({
      status: 'success',
      message: '用戶註冊成功'
    });
  } catch (error) {
    console.error('註冊錯誤:', error);
    res.status(500).json({
      status: 'error',
      message: '註冊過程中發生錯誤'
    });
  }
});

// 用戶登錄路由(使用速率限制)
app.post('/login', loginLimiter, async (req: Request, res: Response) => {
  try {
    const { username, password }: User = req.body;

    // 輸入驗證
    if (!username || !password) {
      return res.status(400).json({
        status: 'error',
        message: '用戶名和密碼都是必需的'
      });
    }

    // 從數據庫獲取用戶
    // const user = await db.findUserByUsername(username);
    // if (!user) {
    //   return res.status(401).json({
    //     status: 'error',
    //     message: '認證失敗'
    //   });
    // }

    // 模擬數據庫中的哈希密碼
    const storedHash = '$2b$10$YourStoredHashHere';

    // 使用時間恆定的比較方法
    const isMatch = await bcrypt.compare(password, storedHash);

    if (!isMatch) {
      return res.status(401).json({
        status: 'error',
        message: '認證失敗'
      });
    }

    // 更新最後登錄時間
    // await db.updateLastLogin(user.id);

    // 生成 JWT 令牌(實際應用中應該使用)
    // const token = generateJWT(user);

    res.status(200).json({
      status: 'success',
      message: '登錄成功',
      // token: token
    });
  } catch (error) {
    console.error('登錄錯誤:', error);
    res.status(500).json({
      status: 'error',
      message: '登錄過程中發生錯誤'
    });
  }
});

// 全局錯誤處理中間件
app.use((err: Error, req: Request, res: Response) => {
  console.error('服務器錯誤:', err);
  res.status(500).json({
    status: 'error',
    message: '服務器內部錯誤'
  });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`服務器運行在端口 ${PORT}`);
});

這個示例包含了以下安全特性:

  1. 輸入驗證:檢查必需字段
  2. 密碼強度要求:實施最小長度限制
  3. 速率限制:防止暴力破解攻擊
  4. 安全的錯誤處理:不洩露敏感信息
  5. TypeScript 支持:提供類型安全
  6. 請求大小限制:防止 DOS 攻擊
  7. 統一的錯誤響應格式:便於前端處理

密碼加密的最佳實踐

選擇適當的工作因子(Rounds)

bcrypt 的工作因子決定了密碼哈希的計算複雜度。選擇合適的工作因子是平衡安全性和性能的關鍵:

  1. 默認值通常為 10,對於大多數應用來說足夠
  2. 理想的工作因子應使密碼驗證時間在約 0.1 秒左右
  3. 根據 2017 年的測試,在 Intel Xeon CPU 上不同 rounds 的處理時間:
    • 8 rounds: 0.028 秒
    • 9 rounds: 0.057 秒
    • 10 rounds: 0.115 秒
    • 11 rounds: 0.227 秒
  4. 對於高安全性需求,可考慮提高到 13+,但需監控性能影響
  5. 隨著硬件性能的提升,應定期重新評估並可能增加工作因子

永不以明文存儲密碼

這是最基本的安全原則。無論應用規模如何,都不應以明文形式存儲密碼。使用 bcrypt 等安全的哈希算法是保護用戶密碼的必要措施。

利用 bcrypt 的鹽值功能

bcrypt 的一個關鍵安全特性是自動鹽值處理。確保:

  1. 每個用戶使用唯一的鹽值
  2. 鹽值應為隨機生成的字符串
  3. 讓 bcrypt 自動處理鹽值,而不是手動管理

這確保即使多個用戶選擇相同的密碼,存儲的哈希值也將完全不同,大大提高了安全性。

實施強密碼政策

除了使用 bcrypt 外,還應考慮:

  1. 不允許用戶使用過於簡單的密碼,如 123456
  2. 實施密碼長度和複雜性要求
  3. 考慮定期密碼更新政策(但注意平衡用戶體驗)

在實際應用中的考慮因素

  1. 性能平衡:根據應用規模和服務器性能調整工作因子
  2. 異步操作:在高流量應用中,優先使用 bcrypt 的異步方法避免阻塞主線程
  3. 錯誤處理:妥善處理加密過程中可能出現的異常
  4. 存儲完整哈希:確保存儲 bcrypt 生成的完整哈希字符串,包括算法標識符、工作因子和鹽值

結論

在密碼安全領域,bcrypt 代表了一種經過時間檢驗的強大解決方案。它的慢速計算特性、自動鹽值處理和可調整的工作因子使其成為 Node.js 應用中密碼存儲的理想選擇。

通過本報告介紹的方法和最佳實踐,開發者可以顯著提高應用的安全性,保護用戶數據免受常見攻擊。記住,密碼安全是一個不斷發展的領域,應定期評估和更新安全措施,以應對新的威脅和挑戰。

在實施密碼安全策略時,bcrypt 只是整體安全架構的一部分。它應與其他安全措施(如 HTTPS、安全的會話管理、適當的訪問控制等)結合使用,以提供全面的應用安全保障。

關於 Calpa

Calpa 擅長使用 TypeScriptReact.jsVue.js 建立 Responsive Website。

他積極參與開源社區,曾在 2019 年的香港開源大會上擔任講者,提供工作經驗和見解。此外,他也在 GitHub 上公開分享個人博客程式碼,已獲得超過 300 顆星星和 60 個分支的支持。

他熱愛學習新技術,並樂意分享經驗。他相信,唯有不斷學習才能跟上快速演變的技術環境。

熱門文章

最新文章

圖片管理中心
管理圖片資源
IP 查詢
快速查詢和定位 IP 地址的地理位置和相關信息
Python 運行器
無需後端、無需登入,只需打開瀏覽器即可運行 Python 代碼(由 Pyodide 提供支持)
封面圖生成器
自動創建適合各種平台的文章封面圖
原作(青山剛昌)產生器
一鍵創建原作(青山剛昌)的封面圖
日本色彩
探索和使用傳統日本色彩
部落格內容洞察儀表板
以視覺化儀表板方式追蹤文章成效、分享熱度與分類分布,協助創作者掌握內容表現。
蒙特卡羅估算 π
使用蒙特卡羅方法演示 π 值的估算過程
LLM
使用 LLM 模型進行聊天
活動圖生成器
一鍵創建活動的封面圖
Wagmi Card
一鍵創建 Wagmi 的封面圖
Facebook Quote
Facebook Quote
Music Macro Language (MML) Studio
用程式語法編寫旋律,用音符構築想像
Blurhash
一鍵創建 Blurhash
文字分類器
使用 MediaPipe TextClassifier 分類文字
前端工程師免費工具資源
前端工程師免費工具資源
後端工程師免費工具資源
後端工程師免費工具資源
全端工程師免費工具資源
全端工程師免費工具資源
Web3 工程師免費工具資源
Web3 工程師免費工具資源
紫微斗數排盤系統|結合 AI 的命盤性格與事業財務分析生成器
紫微斗數排盤工具,輸入生日與時辰,自動生成完整命盤分析提示(Prompt)。結合最專業紫微理論與 AI 助力,助你深入解析性格、事業、財務與人際課題。免費使用,適合命理師及紫微愛好者。
PixAI Prompt 組合器|快速打造可用於 AI 繪圖的語言拼圖
使用 PixAI 卻不會寫 prompt?這個工具幫你一鍵組裝角色、表情、風格語彙,輸出高品質繪圖提示語句(Prompt),可直接貼入 PixAI 使用。適合插畫師、創作者、AI 新手與 VTuber 角色開發者。