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

在 Node.js 中使用 bcrypt 加密密碼的完整指南
作者: Calpa Liu
字數:2729
出版:2025年3月19日
#bcrypt#Node.js#security#database#salt#hashing algorithm

在現代應用程序開發中,安全存儲用戶密碼至關重要。本報告將詳細介紹如何在 Node.js 中使用 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的構造為:

  • 2a:算法標識符
  • 12:成本因子
  • /NkvJmm3v1ua1Mc5s82X5O:鹽值
  • vW0bMVsSy0oTQH4do3BN4Td14pAMTiW:哈希值

為何選擇 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 個分支的支持。

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