bcrypt.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. 'use strict';
  2. const bcrypt = require('bcryptjs');
  3. const crypto = require('crypto');
  4. module.exports = app => {
  5. class Bcrypt extends app.BaseService {
  6. /**
  7. * 生成 Bcrypt 加盐哈希(自动生成随机盐)
  8. * @param {string} plainPassword 明文密码
  9. * @return {Promise<string>} bcrypt 加密后的哈希字符串
  10. */
  11. async generateBcryptHash(plainPassword) {
  12. try {
  13. const salt = await bcrypt.genSalt(10); // 纯 JS 异步生成,无阻塞
  14. // 2. 生成密码哈希(盐值自动拼接在哈希结果中,无需单独存储盐值)
  15. const hashedPassword = await bcrypt.hash(plainPassword, salt);
  16. return hashedPassword;
  17. } catch (error) {
  18. console.error('Bcrypt 哈希生成失败:', error);
  19. throw new Error('密码加密失败'); // 生产环境可封装为自定义错误
  20. }
  21. }
  22. /**
  23. * 验证 Bcrypt 哈希
  24. * @param {string} plainPassword 明文密码
  25. * @param {array<string>} storedBcryptHash 数据库存储的哈希字段数组
  26. * @return {Promise<boolean>} 验证结果
  27. */
  28. async verifyBcryptHash(plainPassword, storedBcryptHash) {
  29. // 支持传入单个哈希字符串(推荐),也兼容老的数组形式
  30. const candidates = Array.isArray(storedBcryptHash) ? storedBcryptHash : [storedBcryptHash];
  31. if (!Array.isArray(candidates) || candidates.length === 0) return false;
  32. for (const raw of candidates) {
  33. if (!raw || typeof raw !== 'string') continue;
  34. const bcryptHash = raw;
  35. // 如果以 bcrypt 前缀开头,则按 bcrypt hash 验证;否则视为加密的明文(使用 decrypt 解密后直接比较明文)
  36. if (bcryptHash.startsWith('$2')) {
  37. try {
  38. const isMatch = await bcrypt.compare(plainPassword, bcryptHash);
  39. if (isMatch) return true;
  40. } catch (error) {
  41. if (app && app.logger && app.logger.warn) app.logger.warn('bcrypt compare error', error);
  42. }
  43. } else {
  44. try {
  45. const decrypted = this.decrypt(bcryptHash);
  46. if (decrypted === plainPassword) return true;
  47. } catch (err) {
  48. if (app && app.logger && app.logger.warn) app.logger.warn('bcrypt decrypt failed for candidate');
  49. continue;
  50. }
  51. }
  52. }
  53. return false;
  54. }
  55. /**
  56. * AES-256-CBC 加密函数(用于存储副密码)
  57. * @param {string} plainText - 明文(客户副密码)
  58. * @return {string} - 加密后的密文(base64 格式,便于数据库存储)
  59. */
  60. encrypt(plainText) {
  61. if (!plainText || typeof plainText !== 'string') {
  62. throw new Error('明文必须为非空字符串');
  63. }
  64. // 创建加密器
  65. const cipher = crypto.createCipheriv(
  66. app.config.aes.algorithm,
  67. Buffer.from(app.config.aes.secretKey, 'utf8'),
  68. Buffer.from(app.config.aes.secretIv, 'utf8')
  69. );
  70. // 执行加密
  71. let encrypted = cipher.update(plainText, 'utf8', 'base64');
  72. encrypted += cipher.final('base64');
  73. return encrypted;
  74. }
  75. /**
  76. * AES-256-CBC 解密函数(用于销售查询副密码)
  77. * @param {string} cipherText - 密文(数据库中存储的加密后副密码)
  78. * @return {string} - 解密后的明文(客户副密码)
  79. */
  80. decrypt(cipherText) {
  81. if (!cipherText || typeof cipherText !== 'string') {
  82. throw new Error('密文必须为非空字符串');
  83. }
  84. // 创建解密器
  85. const decipher = crypto.createDecipheriv(
  86. app.config.aes.algorithm,
  87. Buffer.from(app.config.aes.secretKey, 'utf8'),
  88. Buffer.from(app.config.aes.secretIv, 'utf8')
  89. );
  90. // 执行解密
  91. let decrypted = decipher.update(cipherText, 'base64', 'utf8');
  92. decrypted += decipher.final('utf8');
  93. return decrypted;
  94. }
  95. }
  96. return Bcrypt;
  97. };