如何使用 Passport.js 在 Node.js 中重置/更改密码?
Posted
技术标签:
【中文标题】如何使用 Passport.js 在 Node.js 中重置/更改密码?【英文标题】:How to reset / change password in Node.js with Passport.js? 【发布时间】:2013-12-15 03:13:30 【问题描述】:我在 Node.js 中使用 Passport.js 来创建登录系统。一切都很好,但是当他们忘记密码或想要更改密码时,我不知道如何重置用户密码。
MongoDB 中的用户模型
var UserSchema = new Schema(
email: String,
username: String,
provider: String,
hashed_password: String,
salt: String,
);
【问题讨论】:
这里有一个很棒的教程:sahatyalkabov.com/how-to-implement-password-reset-in-nodejs 【参考方案1】:不太喜欢访问我的数据库来存储令牌的想法,尤其是当您想要为许多操作创建和验证令牌时。
相反,我决定复制Django does it:
将timestamp_today 转换为base36 为today
将 user.id 转换为 base36 为ident
创建hash
包含:
timestamp_today
user.id
user.last_login
用户密码
user.email
用隐藏的秘密为哈希加盐
创建如下路由:/change-password/:ident
/:today
-:hash
我们测试 req.params.timestamp 是为了简单地测试它在今天是否有效,首先是最便宜的测试。先失败。
然后我们找到用户,如果不存在则失败。
然后我们从上面再次生成哈希,但时间戳来自 req.params
如果出现以下情况,重置链接将失效:
他们记住了自己的密码和登录信息(last_login 更改) 他们实际上仍处于登录状态并且: 只需更改他们的密码(密码更改) 只需更改他们的电子邮件(电子邮件更改) 明天到了(时间戳变化太大)这边:
您没有将这些短暂的东西存储在数据库中 如果令牌的目的是改变事物的状态,而事物的状态发生了变化,那么令牌的目的就不再是安全相关的。【讨论】:
我喜欢这个想法,但是,如果您要创建哈希的密码(带有盐),您最终不会得到一个非常长的路由 url,因为哈希会相当长吗? 长哈希?这取决于您对可能发生的碰撞程度感到满意。在大多数情况下,我们不支持无法呈现长链接的损坏软件,这样做只会训练用户忽略安全风险。 我用 node/express 实现了这个。它运作良好,谢谢。 URL 最终少于 200 个字符 - 不会太长。我认为仅使用“今天”的想法在给定时区/一天中的时间不会起作用,但是使用 moment.js 实现时间窗口并不难: var timeStamp = base36.decode(req.body.timeHash) ; var then = moment(timeStamp); var now = moment().utc(); var timeSince = now.diff(then, 'hours');【参考方案2】:我尝试按照 Matt617 的建议使用 node-password-reset,但并不真正关心它。这是目前搜索中唯一出现的内容。
经过几个小时的研究,我发现自己实现这一点更容易。最后,我花了大约一天的时间让所有的路线、用户界面、电子邮件和一切正常工作。我仍然需要稍微增强安全性(重置计数器以防止滥用等),但基础工作正常:
-
创建了两个新路由,/forgot 和 /reset,不需要用户登录即可访问。
/forgot 上的 GET 会显示带有一个电子邮件输入的 UI。
/forgot 上的 POST 检查是否存在具有该地址的用户并生成随机令牌。
使用令牌和到期日期更新用户记录
发送一封电子邮件,其中包含指向 /reset/token 的链接
/reset/token 上的 GET 检查是否有用户使用该令牌未过期,然后显示带有新密码条目的 UI。
/reset 上的 POST(发送新密码和令牌)检查是否有用户使用该令牌尚未过期。
更新用户密码。
将用户的令牌和到期日期设置为空
这是我生成令牌的代码(取自 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;
希望这会有所帮助。
编辑: 这是 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'));
);
编辑: 这是路线。
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');
);
);
【讨论】:
能否上传app.js 看看它是如何与护照结合的?【参考方案3】:这里实现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;
);
在我的应用程序中,我在此帐户模型中使用本地护照
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);
【讨论】:
【参考方案4】:在您的数据库中创建一个随机重置密钥,并将其与时间戳一起保存。然后创建一个接受重置键的新路由。在将密码更改为路由中的新密码之前验证时间戳。
从未尝试过,但我前段时间遇到过,这与您需要的类似: https://github.com/substack/node-password-reset
【讨论】:
我有同样的问题,这个答案对我来说毫无意义。想进一步解释一下吗? 你看过我引用的github中的node-password-reset模块了吗?【参考方案5】:也许这篇文章会有所帮助:
Password Reset Emails In Your React App Made Easy with Nodemailer
【讨论】:
以上是关于如何使用 Passport.js 在 Node.js 中重置/更改密码?的主要内容,如果未能解决你的问题,请参考以下文章
如何在 passport-facebook / Passport.js 中捕获 FacebookAuthorizationError?
如何使用 Passport.js 在 Node.js 中重置/更改密码?
使用 Passport.js 和 Sails.js 注册后如何重定向到特定位置?