TypeScript Mongoose: 在儲存用戶的密碼前先做 hashing
所需套件
- mongoose(ODM:將資料庫的資料結構對應轉換成 Javascript 的 Object,方便操作資料庫的資料)。
- bcrypt(用作產生 salt 和 hashing),Salt: 為密碼加鹽,在密碼上加上隨機生成的字串,增加儲存的密碼的亂度。
// install dependencies
yarn add mongoose bcryptjs
// install dev dependencies
yarn add @types/mongoose, @types/bcryptjs
上下文
假設現有一 user model,在 mongoose 中 model 要先定義好它的綱要 Schema 後再以mongoose.model(Schema)
方法將將其 “編譯”成 model。
// import required package
import mongoose from 'mongoose';
import bcrypt from "bcrypt"
// Schema Definition
const userSchema = new mongoose.Schema(
{
email: { type: String, required: true, unique: true },
name: { type: String, required: true },
password: { type: String, required: true },
},
{
timestamps: true, // mongoose assign createdAt and updatedAt automatically
);
對應的 Type Definition:
export interface IUserDocument extends Document {
email: string;
name: string;
password: string;
createdAt: Date; // we defined timestamp as true in our schema
updatedAt: Date;
comparePassword(candidatePassword: string): Promise<boolean>;
}
在 userSchema 的資料被存到資料庫前,我們希望先把密碼加密,這裏可以引用 mongoose 的 middleware(又被稱爲 pre / post hooks)。在操作文件的 middleware 當中 mongoose 提供了下列幾種類型:
- validate
- save
- remove
- updateOne
- deleteOne
- init
我們要在儲存前處理資料,所以用的是 schema.pre(’save’, function(next){})
,next 的 Type Definition 在 mongoose 6.0版本前爲 HookNextFunction
,要是你使用的版本爲6.0以後直接 implicit any 即可。
在實現加密的部分我們用的是 bcrypt 套件:
bcrypt.genSalt(saltWorkFactor)
方法能產生salt,saltWorkFactor 控制計算單個 BCrypt hash 需要多少時間,數值越高越安全,但設太高會影響用戶體驗(加密時間太長)。
bcrypt.hashSync(user.password, salt)
實際加密密碼的方法。
// hash the password before(hence pre) saving
// arrow function changes the scope of 'this.', so don't use it!
userSchema.pre('save', async function (next) {
let user = this as IUserDocument; // a little trick so don't have to reference this later
// only hash the password if it has been modified (or is new)
if (!user.isModified('password')) {
return next();
}
/*
In our .env saltWorkFactor is setted as 10.
*/
try {
// Salt is the random data that is used as an additional input to a one-way function that hashes data
const salt = await bcrypt.genSalt(+process.env.saltWorkFactor); // saltWorkFactor is a string in .env file, +saltWorkFactor convert it to number
const hash = await bcrypt.hashSync(user.password, salt);
user.password = hash;
return next();
} catch (error) {
return next(error);
}
});
注意不管加密過程中有沒有出現錯誤,跟 express 使用 middleware 的時候相似,middleware結束時都需要呼叫 next()
。
在 Schema 的 methods prototype 中實現驗證密碼用的方法,bcrypt.compare(API傳入的密碼, model裏已加密的密碼)
可以比對字串和已加密的字串,回傳 boolean。
// Password validation method
userSchema.methods.comparePassword = async function (
candidatePassword: string
): Promise<boolean> {
const user = this as IUserDocument;
try {
return bcrypt.compare(candidatePassword, user.password);
} catch (error) {
return false;
}
};
在比對密碼的過程中出錯一律回傳 false。
最後將 Schema “編譯”成 model:
// User Model
const UserModel = model<IUserDocument>('User', userSchema);
export default UserModel;
實測 mongoDB 裏的密碼被已加密:
完整程式碼
import { Document, Schema, model } from 'mongoose';
import bcrypt from 'bcrypt';
export interface IUserDocument extends Document {
email: string;
name: string;
password: string;
createdAt: Date; // timestamps is true, so that we would have createdAt and updatedAt on our schema
updatedAt: Date;
comparePassword(candidatePassword: string): Promise<boolean>;
}
// Schema Definition
const userSchema = new Schema(
{
email: { type: String, required: true, unique: true },
name: { type: String, required: true },
password: { type: String, required: true },
},
{
timestamps: true, // mongoose assign createdAt and updatedAt fields to the schema automatically
}
);
// hash the password before(hence pre) saving
userSchema.pre('save', async function (next) {
let user = this as IUserDocument; // a little trick so don't have to reference this later
// only hash the password if it has been modified (or is new)
if (!user.isModified('password')) {
return next();
}
try {
// salt is the random data that is used as an additional input to a one-way function that hashes data
const salt = await bcrypt.genSalt(+process.env.saltWorkFactor); // saltWorkFactor is a string in .env file, +saltWorkFactor convert it to number
const hash = await bcrypt.hashSync(user.password, salt);
user.password = hash;
return next();
} catch (error) {
return next(error);
}
});
// Password validation method
userSchema.methods.comparePassword = async function (
candidatePassword: string
): Promise<boolean> {
const user = this as IUserDocument;
try {
return bcrypt.compare(candidatePassword, user.password);
} catch (error) {
return false;
}
};
// User Model
const UserModel = model<IUserDocument>('User', userSchema);
export default UserModel;
參考資料
Mongoose v6.2.3: Middleware (https://mongoosejs.com/docs/middleware.html)
Mongoose中介軟體入門示例 (https://www.itread01.com/content/1549650963.html)