Mongoose密码哈希化

63

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

我的问题是:密码被异步地哈希。在这里setter不起作用,因为它只能同步工作。

我考虑了两种方法:

  • 在哈希函数的回调中创建模型实例并保存它。

  • 在“save”上创建一个pre hook。

有没有解决此问题的好方法?

10个回答

182

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
    });
});

19
如果有人尝试使用“update”(就像我最初所做的那样)来执行操作,请注意一点,根据mongoose文档:在update()、findOneAndUpdate()等操作中不会执行预处理和后处理save()钩子。 - k00k
4
可以的。你可以编写一个分别执行find()和save()的函数? - Willem Mulder
2
这种方法仅适用于创建时,当用户更新密码时,它将不会被哈希处理。 - Maxwell s.c
1
在“comparePassword”步骤中,bcrypt如何知道该用户的盐值是什么? - david_adler
3
原来盐被存储在生成的哈希值中。https://github.com/kelektiv/node.bcrypt.js/issues/749 - david_adler
要让这个在更新时起作用,我们只需要这样做:pre("findOneAndUpdate",是吗? - CodeFinity

26

对于那些愿意使用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) 是异步的,但您没有等待任何东西。 - Rubek Joshi
@RubekJoshi 在异步函数中等待进程并不是必要的。在这种情况下,bcrypt.compare() 是一个异步函数,因为它采用标准的 Node 回调,我们可以自动转换为 async/await 语法并返回包含结果的 Promise。函数的使用者需要等待它以获取结果。不过,这是个好问题! - Sohail

8

简而言之 - Typescript 解决方案

我在寻找使用 Typescript 的相同解决方案时到达了这里。因此,对于任何对上述问题感兴趣的人,以下是我最终使用的示例。

导入和常量:

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 },
});

用户模式(pre-save)钩子的实现
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)


2
UserSchema.pre<DIUser> 运行良好,我们也可以使用 'bcrypt' 来提高性能。 - sdykae

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

Result


2

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);
    });
};

1

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

/**
 * 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
@Jools,你关于阻止执行的想法是正确的。相反,使用异步方法,而这个“this._password”是一个虚拟的临时变量,根据文档所述:“在Mongoose中,虚拟属性不会存储在MongoDB中。通常,虚拟属性用于计算文档属性”,因此它不会被存储。 - pkarc

1
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

                        }

1

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


0

我想使用钩子会更好,经过一些研究我发现

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

在这里说:

使用案例:

异步默认值

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


0

我使用了.find({email})而不是.findOne({email})

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

示例:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接