猫鼬密码哈希
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<DIUser>
工作正常,我们也可以使用'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); // -> Password123: true
【讨论】:
【参考方案9】:经过一番研究,我想最好使用钩子
http://mongoosejs.com/docs/middleware.html
上面写着:
用例:
异步默认值
我更喜欢这个解决方案,因为我可以封装它并确保一个帐户只能用密码保存。
【讨论】:
【参考方案10】:我用.find(email)
代替.findOne(email
)。
确保使用.findOne(...)
获取用户。
例子:
const user = await <user>.findOne( email );
【讨论】:
以上是关于猫鼬密码哈希的主要内容,如果未能解决你的问题,请参考以下文章
针对 Scrypt 组合哈希验证 python 密码:(设置+盐+哈希)