【问题标题】:Mongoose password hashing猫鼬密码哈希
【发布时间】:2013-01-13 07:49:31
【问题描述】:

我正在寻找一种使用 mongoose 将帐户保存到 MongoDB 的好方法。

我的问题是:密码是异步散列的。 setter 不能在这里工作,因为它只能同步工作。

我想到了两种方法:

  • 创建模型实例并将其保存在 哈希函数。

  • 在“保存”时创建预挂钩

这个问题有什么好的解决办法吗?

【问题讨论】:

    标签: node.js mongodb mongoose


    【解决方案1】:

    mongodb 博客有一篇很棒的文章详细介绍了如何实现用户身份验证。

    http://blog.mongodb.org/post/32866457221/password-authentication-with-mongoose-part-1

    以下内容是直接从上面的链接复制过来的:

    用户模型

    var mongoose = require('mongoose'),
        Schema = mongoose.Schema,
        bcrypt = require('bcrypt'),
        SALT_WORK_FACTOR = 10;
         
    var UserSchema = new Schema({
        username: { type: String, required: true, index: { unique: true } },
        password: { type: String, required: true }
    });
         
    UserSchema.pre('save', function(next) {
        var user = this;
    
        // only hash the password if it has been modified (or is new)
        if (!user.isModified('password')) return next();
    
        // generate a salt
        bcrypt.genSalt(SALT_WORK_FACTOR, function(err, salt) {
            if (err) return next(err);
    
            // hash the password using our new salt
            bcrypt.hash(user.password, salt, function(err, hash) {
                if (err) return next(err);
                // override the cleartext password with the hashed one
                user.password = hash;
                next();
            });
        });
    });
         
    UserSchema.methods.comparePassword = function(candidatePassword, cb) {
        bcrypt.compare(candidatePassword, this.password, function(err, isMatch) {
            if (err) return cb(err);
            cb(null, isMatch);
        });
    };
         
    module.exports = mongoose.model('User', UserSchema);
    

    用法

    var mongoose = require(mongoose),
        User = require('./user-model');
         
    var connStr = 'mongodb://localhost:27017/mongoose-bcrypt-test';
    mongoose.connect(connStr, function(err) {
        if (err) throw err;
        console.log('Successfully connected to MongoDB');
    });
         
    // create a user a new user
    var testUser = new User({
        username: 'jmar777',
        password: 'Password123'
    });
         
    // save the user to database
    testUser.save(function(err) {
        if (err) throw err;
    });
        
    // fetch the user and test password verification
    User.findOne({ username: 'jmar777' }, function(err, user) {
        if (err) throw err;
         
        // test a matching password
        user.comparePassword('Password123', function(err, isMatch) {
            if (err) throw err;
            console.log('Password123:', isMatch); // -> Password123: true
        });
         
        // test a failing password
        user.comparePassword('123Password', function(err, isMatch) {
            if (err) throw err;
            console.log('123Password:', isMatch); // -> 123Password: false
        });
    });
    

    【讨论】:

    • 只是给那些可能试图通过“更新”(我最初所做的)失败的人的注释,来自 mongoose 文档:更新时不执行 Pre 和 post save() 钩子()、findOneAndUpdate() 等
    • 正确。你能做的是一个单独的 find() 然后 save() 函数?
    • 该方法只在创建时有效,用户更新密码时不会被哈希
    • comparePassword 步骤中,bcrypt 如何知道该用户的盐是什么?
    • 原来盐存储在生成的哈希github.com/kelektiv/node.bcrypt.js/issues/749
    【解决方案2】:

    愿意用ES6+语法的可以用这个——

    const bcrypt = require('bcryptjs');
    const mongoose = require('mongoose');
    const { isEmail } = require('validator');
    
    const { Schema } = mongoose;
    const SALT_WORK_FACTOR = 10;
    
    const schema = new Schema({
      email: {
        type: String,
        required: true,
        validate: [isEmail, 'invalid email'],
        createIndexes: { unique: true },
      },
      password: { type: String, required: true },
    });
    
    schema.pre('save', async function save(next) {
      if (!this.isModified('password')) return next();
      try {
        const salt = await bcrypt.genSalt(SALT_WORK_FACTOR);
        this.password = await bcrypt.hash(this.password, salt);
        return next();
      } catch (err) {
        return next(err);
      }
    });
    
    schema.methods.validatePassword = async function validatePassword(data) {
      return bcrypt.compare(data, this.password);
    };
    
    const Model = mongoose.model('User', schema);
    
    module.exports = Model;
    

    【讨论】:

    • validatePassword(data) 是异步的,但你没有在等待任何东西。
    • @RubekJoshi 不需要等待异步函数中的进程。在这种情况下,bcrypt.compare() 是一个异步函数,因为它采用标准的 Node 回调,我们可以自动转换为 async/await 语法并返回包含结果的 promise。函数的使用者需要等待它才能得到结果。好问题!
    【解决方案3】:

    TL;DR - Typescript 解决方案

    当我在寻找相同的解决方案但使用打字稿时,我来到了这里。因此,对于任何对上述问题的 TS 解决方案感兴趣的人,这里有一个我最终使用的示例。

    导入 && 内容:

    import mongoose, { Document, Schema, HookNextFunction } from 'mongoose';
    import bcrypt from 'bcryptjs';
    
    const HASH_ROUNDS = 10;
    

    简单的用户界面和架构定义:

    export interface IUser extends Document {
        name: string;
        email: string;
        password: string;
        validatePassword(password: string): boolean;
    }
    
    const userSchema = new Schema({
        name: { type: String, required: true },
        email: { type: String, required: true, unique: true },
        password: { type: String, required: true },
    });
    

    用户架构预保存挂钩实现

    userSchema.pre('save', async function (next: HookNextFunction) {
        // here we need to retype 'this' because by default it is 
        // of type Document from which the 'IUser' interface is inheriting 
        // but the Document does not know about our password property
        const thisObj = this as IUser;
    
        if (!this.isModified('password')) {
            return next();
        }
    
        try {
            const salt = await bcrypt.genSalt(HASH_ROUNDS);
            thisObj.password = await bcrypt.hash(thisObj.password, salt);
            return next();
        } catch (e) {
            return next(e);
        }
    });
    

    密码验证方法

    userSchema.methods.validatePassword = async function (pass: string) {
        return bcrypt.compare(pass, this.password);
    };
    

    和默认导出

    export default mongoose.model<IUser>('User', userSchema);
    

    注意:不要忘记安装类型包(@types/mongoose@types/bcryptjs

    【讨论】:

    • UserSchema.pre&lt;DIUser&gt; 工作正常,我们也可以使用'bcrypt'来提高性能。
    【解决方案4】:

    我认为这是用户Mongoose和bcrypt的好方法!

    用户模型

    /**
     * Module dependences
    */
    
    const mongoose = require('mongoose');
    const Schema = mongoose.Schema;
    const bcrypt = require('bcrypt');
    const SALT_WORK_FACTOR = 10;
    
    // define User Schema
    const UserSchema = new Schema({
        username: {
            type: String,
            unique: true,
            index: {
                unique: true
            }
        },
        hashed_password: {
            type: String,
            default: ''
        }
    });
    
    // Virtuals
    UserSchema
        .virtual('password')
        // set methods
        .set(function (password) {
            this._password = password;
        });
    
    UserSchema.pre("save", function (next) {
        // store reference
        const user = this;
        if (user._password === undefined) {
            return next();
        }
        bcrypt.genSalt(SALT_WORK_FACTOR, function (err, salt) {
            if (err) console.log(err);
            // hash the password using our new salt
            bcrypt.hash(user._password, salt, function (err, hash) {
                if (err) console.log(err);
                user.hashed_password = hash;
                next();
            });
        });
    });
    
    /**
     * Methods
    */
    UserSchema.methods = {
        comparePassword: function(candidatePassword, cb) {
            bcrypt.compare(candidatePassword, this.password, function(err, isMatch) {
                if (err) return cb(err);
                cb(null, isMatch);
            });
        };
    }
    
    module.exports = mongoose.model('User', UserSchema);
    

    用法

    signup: (req, res) => {
        let newUser = new User({
            username: req.body.username,
            password: req.body.password
        });
        // save user
        newUser.save((err, user) => {
            if (err) throw err;
            res.json(user);
        });
    }
    

    结果

    Result

    【讨论】:

      【解决方案5】:

      Mongoose 官方解决方案要求在使用 verifyPass 方法前先保存模型,这会造成混淆。以下内容对您有用吗? (我使用的是 scrypt 而不是 bcrypt)。

      userSchema.virtual('pass').set(function(password) {
          this._password = password;
      });
      
      userSchema.pre('save', function(next) {
          if (this._password === undefined)
              return next();
      
          var pwBuf = new Buffer(this._password);
          var params = scrypt.params(0.1);
          scrypt.hash(pwBuf, params, function(err, hash) {
              if (err)
                  return next(err);
              this.pwHash = hash;
              next();
          });
      });
      
      userSchema.methods.verifyPass = function(password, cb) {
          if (this._password !== undefined)
              return cb(null, this._password === password);
      
          var pwBuf = new Buffer(password);
          scrypt.verify(this.pwHash, pwBuf, function(err, isMatch) {
              return cb(null, !err && isMatch);
          });
      };
      

      【讨论】:

        【解决方案6】:

        使用虚拟和实例方法的另一种方法:

        /**
         * Virtuals
         */
        schema.virtual('clean_password')
            .set(function(clean_password) {
                this._password = clean_password;
                this.password = this.encryptPassword(clean_password);
            })
            .get(function() {
                return this._password;
            });
        
        schema.methods = {
        
            /**
             * Authenticate - check if the passwords are the same
             *
             * @param {String} plainText
             * @return {Boolean}
             * @api public
             */
            authenticate: function(plainPassword) {
                return bcrypt.compareSync(plainPassword, this.password);
            },
        
            /**
             * Encrypt password
             *
             * @param {String} password
             * @return {String}
             * @api public
             */
            encryptPassword: function(password) {
                if (!password)
                    return '';
        
                return bcrypt.hashSync(password, 10);
            }
        };
        

        只要保存你的模型,虚拟就可以完成它的工作。

        var user = {
            username: "admin",
            clean_password: "qwerty"
        }
        
        User.create(user, function(err,doc){});
        

        【讨论】:

        • 关于这个的几个问题 - 在完成之前不会使用 bcrypt 的同步方法阻止服务器上的执行(因此可能导致并发用户访问的性能问题)?还有,“this._password”是做什么用的?这会成为架构的一部分吗?
        • @Jools 你对阻塞执行的权利,而不是使用异步方法,this._password 它是虚拟的临时变量,根据文档:“在 Mongoose 中,虚拟是不是存储在 MongoDB 中。虚拟通常用于文档上的计算属性”,因此不会被存储。
        【解决方案7】:

        const bcrypt = require('bcrypt');
        
        const saltRounds = 5;
        const salt = bcrypt.genSaltSync(saltRounds);
        
        module.exports = (password) => {
          return bcrypt.hashSync(password, salt);
        }

        const mongoose = require('mongoose')
        const Schema = mongoose.Schema
        const hashPassword = require('../helpers/hashPassword')
        
        const userSchema = new Schema({
          name: String,
          email: {
            type: String,
            match: [/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, `Please fill valid email address`],
            validate: {
              validator: function() {
                return new Promise((res, rej) =>{
                  User.findOne({email: this.email, _id: {$ne: this._id}})
                      .then(data => {
                          if(data) {
                              res(false)
                          } else {
                              res(true)
                          }
                      })
                      .catch(err => {
                          res(false)
                      })
                })
              }, message: 'Email Already Taken'
            }
          },
          password: {
            type: String,
            required: [true, 'Password required']
          }
        });
        
        userSchema.pre('save', function (next) {
          if (this.password) {
              this.password = hashPassword(this.password)
          }
          next()
        })
        
        const User = mongoose.model('User', userSchema)
        
        module.exports = User

        【讨论】:

          【解决方案8】:
          const mongoose = require('mongoose');
          var bcrypt = require('bcrypt-nodejs');
          SALT_WORK_FACTOR = 10;
          
          const userDataModal = mongoose.Schema({
              username: {
                  type: String,
                  required : true,
                  unique:true
              },
              password: {
                  type: String,
                  required : true
              }
          
          });
          
          userDataModal.pre('save', function(next) {
              var user = this;
          
              // only hash the password if it has been modified (or is new)
              if (!user.isModified('password')) return next();
          
              // generate a salt
              bcrypt.genSalt(SALT_WORK_FACTOR, function(err, salt) {
                  if (err) return next(err);
          
                  // hash the password using our new salt
                  bcrypt.hash(user.password, salt, null, function(err, hash) {
                      if (err) return next(err);
          
                      // override the cleartext password with the hashed one
                      user.password = hash;
                      next();
                  });
              });
          });
          
          userDataModal.methods.comparePassword = function(candidatePassword, cb) {
              bcrypt.compare(candidatePassword, this.password, function(err, isMatch) {
                  if (err) return cb(err);
                  cb(null, isMatch);
              });
          };
          
          
          // Users.index({ emaiId: "emaiId", fname : "fname", lname: "lname" });
          
          const userDatamodal = module.exports = mongoose.model("usertemplates" , userDataModal)
          
          
          
          //inserting document
               userDataModel.findOne({ username: reqData.username }).then(doc => {
                      console.log(doc)
                      if (doc == null) {
                          let userDataMode = new userDataModel(reqData);
                         // userDataMode.password = userDataMode.generateHash(reqData.password);
                          userDataMode.save({new:true}).then(data=>{
                                    let obj={
                                        success:true,
                                        message: "New user registered successfully",
                                        data:data
                                    }
                                      resolve(obj)
                          }).catch(err=>{
                                          reject(err)
                          })
          
                      }
                      else {
                          resolve({
                              success: true,
                              docExists: true,
                              message: "already user registered",
                              data: doc
                          }
                          )
                      }
          
                  }).catch(err => {
                      console.log(err)
                      reject(err)
                  })
          
          //retriving and checking
                // test a matching password
                          user.comparePassword(requestData.password, function(err, isMatch) {
                              if (err){ 
          
                                  reject({
                                      'status': 'Error',
                                      'data': err
                                  });
          
                                  throw err;
                              } else  {
                                  if(isMatch){
          
                                      resolve({   
                                          'status': true,
                                          'data': user,
                                          'loginStatus' : "successfully Login"
                                      });
          
                                      console.log('Password123:', isMatch); // -&gt; Password123: true
          
                                  }
          

          【讨论】:

            【解决方案9】:

            经过一番研究,我想最好使用钩子

            http://mongoosejs.com/docs/middleware.html

            上面写着:

            用例:

            异步默认值

            我更喜欢这个解决方案,因为我可以封装它并确保一个帐户只能用密码保存。

            【讨论】:

              【解决方案10】:

              我用.find({email}) 代替.findOne({email})。

              确保使用.findOne(...) 获取用户。

              例子:

              const user = await <user>.findOne({ email });
              

              【讨论】:

                猜你喜欢
                • 2017-03-08
                • 2017-03-10
                • 2012-07-07
                • 1970-01-01
                • 1970-01-01
                • 2023-04-08
                • 2015-11-30
                • 2018-01-31
                • 2020-04-19
                相关资源
                最近更新 更多