猫鼬密码哈希

Posted

技术标签:

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

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

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

我想到了两种方法:

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

在“保存”时创建预挂钩

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

【问题讨论】:

【参考方案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 );

【讨论】:

以上是关于猫鼬密码哈希的主要内容,如果未能解决你的问题,请参考以下文章

windows密码哈希的sam原理

密码哈希身份框架和盐

针对 Scrypt 组合哈希验证 python 密码:(设置+盐+哈希)

使用密码哈希时生成的哈希的最大长度?

Django 4.0 正式发布,新的密码哈希器和 Redis 缓存后端

如何迁移密码哈希?