如何使用 Passport.js 在 Node.js 中重置/更改密码?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/20277020/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How to reset / change password in Node.js with Passport.js?
提问by user3044147
I use Passport.js in Node.js to create a login system. Everything is ok, but I do not know how to reset user password when they forget their password or they want to change it.
我在 Node.js 中使用 Passport.js 来创建登录系统。一切正常,但我不知道如何在用户忘记密码或想要更改密码时重置用户密码。
User model in MongoDB
MongoDB 中的用户模型
var UserSchema = new Schema({
email: String,
username: String,
provider: String,
hashed_password: String,
salt: String,
});
回答by airtonix
Didn't really like the idea of hitting my database to store tokens, especially when you want to be creating and verifying tokens for many actions.
不太喜欢访问我的数据库来存储令牌的想法,尤其是当您想为许多操作创建和验证令牌时。
Instead I decided to copy how Django does it:
相反,我决定复制Django 的做法:
- convert timestamp_today to base36 as
today - convert user.id to base36 as
ident - create
hashcontaining:- timestamp_today
- user.id
- user.last_login
- user.password
- user.email
- salt the hash with a hidden secret
- create a route like : /change-password/
:ident/:today-:hash
- 将 timestamp_today 转换为 base36 作为
today - 将 user.id 转换为 base36 作为
ident - 创建
hash包含:- 时间戳_今天
- 用户身份
- user.last_login
- 用户密码
- 用户邮箱
- 用隐藏的秘密给哈希加盐
- 创建一个路由,如:/change-password/
:ident/:today-:hash
We test the req.params.timestamp in order to simply test if it's valid for today, cheapest test first. fail first.
我们测试 req.params.timestamp 以简单地测试它是否对今天有效,首先是最便宜的测试。先失败。
Then we find the user, fail if it doesn't exist.
然后我们找到用户,如果它不存在则失败。
Then we generate the hash again from above, but with the timestamp from req.params
然后我们再次从上面生成散列,但使用来自 req.params 的时间戳
The reset link becomes invalid if :
如果出现以下情况,重置链接将无效:
- they remember their password and login (last_login changes)
- they're actually still logged in and:
- just change their password (password changes)
- just change their email (email changes)
- tomorrow arrives (timestamp changes too much)
- 他们记住他们的密码和登录名(last_login 更改)
- 他们实际上仍然登录并且:
- 只需更改他们的密码(密码更改)
- 只需更改他们的电子邮件(电子邮件更改)
- 明天到了(时间戳变化太大)
This way:
这边走:
- you're not storing these ephemeral things in your database
- when the purpose of the token is to change the state of a thing, and that things state changed, then the purpose of the token is no longer securely relevant.
- 你没有将这些短暂的东西存储在你的数据库中
- 当令牌的目的是改变事物的状态,并且事物状态发生变化时,令牌的目的就不再安全相关。
回答by james
I tried to use node-password-reset as Matt617 suggested but didn't really care for it. It's about the only thing that's coming up in searches currently.
我尝试按照 Matt617 的建议使用 node-password-reset,但并不真正关心它。这是目前搜索中唯一出现的东西。
So some hours digging around, I found it easier to implement this on my own. In the end it took me about a day to get all the routes, UI, emails and everything working. I still need to enhance security a bit (reset counters to prevent abuse, etc.) but got the basics working:
因此,经过几个小时的挖掘,我发现自己更容易实现这一点。最后,我花了大约一天的时间来完成所有的路由、UI、电子邮件和一切工作。我仍然需要稍微增强安全性(重置计数器以防止滥用等),但基础工作:
- Created two new routes, /forgot and /reset, which don't require the user to be logged in to access.
- A GET on /forgot displays a UI with one input for email.
- A POST on /forgot checks that there is a user with that address and generates a random token.
- Update the user's record with the token and expiry date
- Send an email with a link to /reset/{token}
- A GET on /reset/{token} checks that there is a user with that token which hasn't expired then shows the UI with new password entry.
- A POST on /reset (sends new pwd and token) checks that there is a user with that token which hasn't expired.
- Update the user's password.
- Set the user's token and expiry date to null
- 创建了两个新路由,/forgot 和 /reset,不需要用户登录即可访问。
- GET on /forgot 显示一个 UI,其中包含一个电子邮件输入。
- /forgot 上的 POST 检查是否存在具有该地址的用户并生成随机令牌。
- 使用令牌和到期日期更新用户记录
- 发送带有 /reset/{token} 链接的电子邮件
- /reset/{token} 上的 GET 检查是否有用户使用该令牌未过期,然后显示带有新密码条目的 UI。
- /reset 上的 POST(发送新密码和令牌)检查是否有用户使用该令牌未过期。
- 更新用户的密码。
- 将用户的令牌和到期日期设置为空
Here's my code for generating a token (taken from node-password-reset):
这是我生成令牌的代码(取自 node-password-reset):
function generateToken() {
var buf = new Buffer(16);
for (var i = 0; i < buf.length; i++) {
buf[i] = Math.floor(Math.random() * 256);
}
var id = buf.toString('base64');
return id;
}
Hope this helps.
希望这可以帮助。
EDIT: Here's the app.js. Note I'm keeping the entire user object in the session. I plan on moving to couchbase or similar in the future.
编辑:这是 app.js。注意我将整个用户对象保留在会话中。我计划将来搬到沙发基地或类似的地方。
var express = require('express');
var path = require('path');
var favicon = require('static-favicon');
var flash = require('connect-flash');
var morgan = require('morgan');
var cookieParser = require('cookie-parser');
var cookieSession = require('cookie-session');
var bodyParser = require('body-parser');
var http = require('http');
var https = require('https');
var fs = require('fs');
var path = require('path');
var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
var app = express();
app.set('port', 3000);
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
var cookies = cookieSession({
name: 'abc123',
secret: 'mysecret',
maxage: 10 * 60 * 1000
});
app.use(cookies);
app.use(favicon());
app.use(flash());
app.use(morgan());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded());
app.use(cookieParser());
app.use(passport.initialize());
app.use(passport.session());
app.use(express.static(path.join(__dirname, 'public')));
module.exports = app;
passport.use(new LocalStrategy(function (username, password, done) {
return users.validateUser(username, password, done);
}));
//KEEP ENTIRE USER OBJECT IN THE SESSION
passport.serializeUser(function (user, done) {
done(null, user);
});
passport.deserializeUser(function (user, done) {
done(null, user);
});
//Error handling after everything else
app.use(logErrors); //log all errors
app.use(clientErrorHandler); //special handler for xhr
app.use(errorHandler); //basic handler
http.createServer(app).listen(app.get('port'), function () {
console.log('Express server listening on HTTP port ' + app.get('port'));
});
EDIT: Here are the routes.
编辑:这是路线。
app.get('/forgot', function (req, res) {
if (req.isAuthenticated()) {
//user is alreay logged in
return res.redirect('/');
}
//UI with one input for email
res.render('forgot');
});
app.post('/forgot', function (req, res) {
if (req.isAuthenticated()) {
//user is alreay logged in
return res.redirect('/');
}
users.forgot(req, res, function (err) {
if (err) {
req.flash('error', err);
}
else {
req.flash('success', 'Please check your email for further instructions.');
}
res.redirect('/');
});
});
app.get('/reset/:token', function (req, res) {
if (req.isAuthenticated()) {
//user is alreay logged in
return res.redirect('/');
}
var token = req.params.token;
users.checkReset(token, req, res, function (err, data) {
if (err)
req.flash('error', err);
//show the UI with new password entry
res.render('reset');
});
});
app.post('/reset', function (req, res) {
if (req.isAuthenticated()) {
//user is alreay logged in
return res.redirect('/');
}
users.reset(req, res, function (err) {
if (err) {
req.flash('error', err);
return res.redirect('/reset');
}
else {
req.flash('success', 'Password successfully reset. Please login using new password.');
return res.redirect('/login');
}
});
});
回答by Matt617
create a random reset key in your DB, persist it with a timestamp. then create a new route that accepts the reset key. verify the timestamp before changing the password to the new password from the route.
在您的数据库中创建一个随机重置密钥,并使用时间戳将其持久化。然后创建一个接受重置密钥的新路由。在将密码更改为路由中的新密码之前验证时间戳。
never tried this but i ran across this some time ago which is similar to what you need: https://github.com/substack/node-password-reset
从未尝试过,但我前段时间遇到过这个,这与您需要的类似:https: //github.com/substack/node-password-reset
回答by Aleksandr Golovatyi
Maybe this article could help:
也许这篇文章可以帮助:
Password Reset Emails In Your React App Made Easy with Nodemailer
回答by BossOz
Here the implementation of airtonix
这里是airtonix的实现
const base64Encode = (data) => {
let buff = new Buffer.from(data);
return buff.toString('base64');
}
const base64Decode = (data) => {
let buff = new Buffer.from(data, 'base64');
return buff.toString('ascii');
}
const sha256 = (salt, password) => {
var hash = crypto.createHash('sha512', password);
hash.update(salt);
var value = hash.digest('hex');
return value;
}
api.post('/password-reset', (req, res) => {
try {
const email = req.body.email;
// Getting the user, only if active
let query = AccountModel.where( {username: email, active: true} );
query.select("_id salt username lastLoginDate");
query.findOne((err, account) => {
if(err) {
writeLog("ERROR", req.url + " - Error: -1 " + err.message);
res.status(500).send( { error: err.message, errnum: -1 } );
return;
}
if(!account){
writeLog("TRACE",req.url + " - Account not found!");
res.status(404).send( { error: "Account not found!", errnum: -2 } );
return;
}
// Generate the necessary data for the link
const today = base64Encode(new Date().toISOString());
const ident = base64Encode(account._id.toString());
const data = {
today: today,
userId: account._id,
lastLogin: account.lastLoginDate.toISOString(),
password: account.salt,
email: account.username
};
const hash = sha256(JSON.stringify(data), process.env.TOKENSECRET);
//HERE SEND AN EMAIL TO THE ACCOUNT
return;
});
} catch (err) {
writeLog("ERROR",req.url + " - Unexpected error during the password reset process. " + err.message);
res.status(500).send( { error: "Unexpected error during the password reset process :| " + err.message, errnum: -99 } );
return;
}
});
api.get('/password-change/:ident/:today-:hash', (req, res) => {
try {
// Check if the link in not out of date
const today = base64Decode(req.params.today);
const then = moment(today);
const now = moment().utc();
const timeSince = now.diff(then, 'hours');
if(timeSince > 2) {
writeLog("ERROR", req.url + " - The link is invalid. Err -1");
res.status(500).send( { error: "The link is invalid.", errnum: -1 } );
return;
}
const userId = base64Decode(req.params.ident);
// Getting the user, only if active
let query = AccountModel.where( {_id: userId, active: true} );
query.select("_id salt username lastLoginDate");
query.findOne((err, account) => {
if(err) {
writeLog("ERROR", req.url + " - Error: -2 " + err.message);
res.status(500).send( { error: err.message, errnum: -2 } );
return;
}
if(!account){
writeLog("TRACE", req.url + " - Account not found! Err -3");
res.status(404).send( { error: "Account not found!", errnum: -3 } );
return;
}
// Hash again all the data to compare it with the link
// THe link in invalid when:
// 1. If the lastLoginDate is changed, user has already do a login
// 2. If the salt is changed, the user has already changed the password
const data = {
today: req.params.today,
userId: account._id,
lastLogin: account.lastLoginDate.toISOString(),
password: account.salt,
email: account.username
};
const hash = sha256(JSON.stringify(data), process.env.TOKENSECRET);
if(hash !== req.params.hash) {
writeLog("ERROR", req.url + " - The link is invalid. Err -4");
res.status(500).send( { error: "The link is invalid.", errnum: -4 } );
return;
}
//HERE REDIRECT TO THE CHANGE PASSWORD FORM
});
} catch (err) {
writeLog("ERROR",req.url + " - Unexpected error during the password reset process. " + err.message);
res.status(500).send( { error: "Unexpected error during the password reset process :| " + err.message, errnum: -99 } );
return;
}
});
In my application I'm using passport-local with this Account model
在我的应用程序中,我使用带有此帐户模型的本地护照
import mongoose from 'mongoose';
import passportLocalMongoose from 'passport-local-mongoose';
const Schema = mongoose.Schema;
let accountSchema = new Schema ({
active: { type: Boolean, default: false },
activationDate: { type: Date },
signupDate: { type: Date },
lastLoginDate: { type: Date },
lastLogoutDate: { type: Date },
salt: {type: String},
hash: {type: String}
});
accountSchema.plugin(passportLocalMongoose); // attach the passport-local-mongoose plugin
module.exports = mongoose.model('Account', accountSchema);

