Passwordless 是一个用于 Node.js 的身份验证中间件,它能提高用户安全性,同时部署快速且简单。
近几个月来,对所有对 Web 安全和隐私感兴趣的人来说,都是令人兴奋的:出现了很多精彩的文章、讨论 和演讲,同时也有大量的事件 加强了人们的意识。
然而,大多数网站仍然使用着从 Web 早期时代就存在的相同身份验证机制:用户名和密码。
虽然用户名和密码有其作用,但我们应该更加谨慎地考虑它们是否是项目中正确的解决方案。我们知道,大多数人在访问所有网站时都使用相同的密码。对于没有专门安全专家的项目,我们真的应该让用户冒着网站被入侵也会损害其亚马逊账户的风险吗?此外,传统机制默认情况下至少有两个攻击途径:登录页面和密码恢复页面。尤其是后者,通常是仓促实现的,因此天生就更具风险。
我们最近看到了不少好主意,其中一个非常直接且低技术的解决方案特别吸引了我:一次性密码。它们实现起来很快,攻击面很小,既不需要 QR 码也不需要 JavaScript。每当用户想要登录或会话失效时,她都会通过电子邮件或短信收到一个带有令牌的短期一次性链接。如果您想尝试一下,欢迎在 passwordless.net 上测试演示。
不幸的是,根据您的技术栈,现成的解决方案并不多。 Passwordless 为 Node.js 改变了这一现状。
Node.js 和 Express 入门
使用 Passwordless 入门非常简单,您可以在两个小时内为小型项目部署一个完整的安全身份验证解决方案。
$ npm install passwordless --save
提供了基本的框架。您还需要安装现有的存储接口之一,例如MongoStore,它可以安全地存储令牌。
$ npm install passwordless-mongostore --save
为了将令牌传递给用户,电子邮件是最常见的选项(但短信也是可行的),您可以自由选择任何现有的电子邮件框架,例如
$ npm install emailjs --save
设置基础
让我们在您用来初始化 Express 的同一个文件中,引入上面提到的所有模块。
var passwordless = require('passwordless');
var MongoStore = require('passwordless-mongostore');
var email = require("emailjs");
如果您选择了emailjs 来传递令牌,那么现在也是将它连接到您的电子邮件帐户(例如 Gmail 帐户)的好时机。
var smtpServer = email.server.connect({
user: yourEmail,
password: yourPwd,
host: yourSmtp,
ssl: true
});
最后一步是告诉 Passwordless 您在上面选择哪个存储接口,并进行初始化。
// Your MongoDB TokenStore
var pathToMongoDb = 'mongodb://localhost/passwordless-simple-mail';
passwordless.init(new MongoStore(pathToMongoDb));
传递令牌
passwordless.addDelivery(deliver)
添加了一个新的传递机制。 deliver
在需要发送令牌时被调用。默认情况下,您选择的机制应该以以下格式向用户提供链接。
http://www.example.com/token={TOKEN}&uid={UID}
deliver
将使用所有必要的细节被调用。因此,令牌的传递(在本例中使用 emailjs)可以像这样简单。
passwordless.addDelivery(
function(tokenToSend, uidToSend, recipient, callback) {
var host = 'localhost:3000';
smtpServer.send({
text: 'Hello!nAccess your account here: http://'
+ host + '?token=' + tokenToSend + '&uid='
+ encodeURIComponent(uidToSend),
from: yourEmail,
to: recipient,
subject: 'Token for ' + host
}, function(err, message) {
if(err) {
console.log(err);
}
callback(err);
});
});
初始化 Express 中间件
app.use(passwordless.sessionSupport());
app.use(passwordless.acceptToken({ successRedirect: '/'}));
sessionSupport()
使登录持久化,因此用户在浏览您的网站时将保持登录状态。请确保您之前已准备好会话中间件(例如 express-session)。
acceptToken()
将拦截任何传入的令牌,对用户进行身份验证,并将他们重定向到正确的页面。虽然 successRedirect
选项不是严格需要的,但强烈建议使用它来避免通过您网站上的外发 HTTP 链接的引用标头泄露有效令牌。
路由和身份验证
以下内容假定您已经按照express 文档 中的说明设置了路由器 var router = express.Router();
。
您至少需要两个 URL 来
- 显示一个页面,要求用户输入他们的电子邮件地址
- 接受表单详细信息(通过 POST)
/* GET: login screen */
router.get('/login', function(req, res) {
res.render('login');
});
/* POST: login details */
router.post('/sendtoken',
function(req, res, next) {
// TODO: Input validation
},
// Turn the email address into a user ID
passwordless.requestToken(
function(user, delivery, callback) {
// E.g. if you have a User model:
User.findUser(email, function(error, user) {
if(error) {
callback(error.toString());
} else if(user) {
// return the user ID to Passwordless
callback(null, user.id);
} else {
// If the user couldn’t be found: Create it!
// You can also implement a dedicated route
// to e.g. capture more user details
User.createUser(email, '', '',
function(error, user) {
if(error) {
callback(error.toString());
} else {
callback(null, user.id);
}
})
}
})
}),
function(req, res) {
// Success! Tell your users that their token is on its way
res.render('sent');
});
这里发生了什么?passwordless.requestToken(getUserId)
有两个任务:确保电子邮件地址存在以及将它转换为一个唯一的用户 ID,可以通过电子邮件发送,并可用于以后识别用户。通常情况下,您已经有一个模型负责存储您的用户详细信息,您可以像上面的示例中那样与它进行交互。
在某些情况下(想想一个由少数用户编辑的博客),您也可以完全跳过用户模型,直接将有效的电子邮件地址与其相应的 ID 硬编码在一起。
var users = [
{ id: 1, email: 'marc@example.com' },
{ id: 2, email: 'alice@example.com' }
];
/* POST: login details */
router.post('/sendtoken',
passwordless.requestToken(
function(user, delivery, callback) {
for (var i = users.length - 1; i >= 0; i--) {
if(users[i].email === user.toLowerCase()) {
return callback(null, users[i].id);
}
}
callback(null, null);
}),
// Same as above…
HTML 页面
它只需要一个简单的 HTML 表单,用来捕获用户的电子邮件地址。默认情况下,Passwordless 会查找一个名为 user
的输入字段。
Login
保护您的页面
Passwordless 提供中间件,以确保只有经过身份验证的用户才能看到某些页面。
/* Protect a single page */
router.get('/restricted', passwordless.restricted(),
function(req, res) {
// render the secret page
});
/* Protect a path with all its children */
router.use('/admin', passwordless.restricted());
谁登录了?
默认情况下,Passwordless 通过请求对象提供用户 ID:req.user
。要显示或重用 ID,将其拉取数据库中的其他详细信息,您可以执行以下操作。
router.get('/admin', passwordless.restricted(),
function(req, res) {
res.render('admin', { user: req.user });
});
或者,更一般地说,您可以添加另一个中间件,从您的模型中拉取整个用户记录,并将其提供给您网站上的任何路由。
app.use(function(req, res, next) {
if(req.user) {
User.findById(req.user, function(error, user) {
res.locals.user = user;
next();
});
} else {
next();
}
})
就这样!
这就是让您的用户安全轻松地进行身份验证所需的全部内容。有关更多详细信息,您应该查看深入探讨,其中解释了所有选项和示例,它将向您展示如何将所有上述内容集成到一个有效的解决方案中。
评估
如前所述,所有身份验证系统都有其权衡,您应该根据自己的需求选择合适的系统。基于令牌的通道与大多数其他解决方案(包括经典的用户名/密码方案)共享一个风险:如果用户的电子邮件帐户被盗取,或者您 SMTP 服务器和用户之间的通道被盗取,那么用户在您网站上的帐户也会被盗取。有两个默认选项可以帮助降低(但不能完全消除!)这种风险:短期令牌和使用后自动使令牌失效。
对于大多数网站来说,基于令牌的身份验证代表着安全性的提升:用户不必想新的密码(通常太简单),并且没有用户重复使用密码的风险。对于我们开发者来说,Passwordless 提供了一个只有一种(且简单!)身份验证路径的解决方案,更容易理解,因此更容易保护。此外,我们不需要接触任何用户密码。
另一个要点是可用性。我们应该考虑第一次使用您的网站和后续登录两种情况。对于第一次使用您的网站的用户,基于令牌的身份验证再简单不过了:他们仍然需要验证他们的电子邮件地址,就像他们使用传统登录机制一样,但在最佳情况下,不需要任何额外的信息。不需要发挥创造力来想出符合所有限制条件的密码,也不需要记住任何东西。如果用户再次登录,体验将取决于具体的用例。大多数网站都有比较长的会话超时时间,登录相对较少。或者,人们访问网站的频率实际上很低,以至于他们难以回忆起自己是否曾经拥有过帐户,如果是的话,密码是什么。在这些情况下,Passwordless 在可用性方面具有明显优势。此外,需要执行的步骤很少,而且这些步骤可以在整个过程中非常清楚地解释。用户经常访问的网站或已经习惯每周登录几次的网站(想想亚马逊)可能更适合使用传统的(甚至更好的:双重验证)方法,因为人们可能会知道他们的密码,并且可能会有更多机会说服用户重视良好密码的重要性。
虽然 Passwordless 被认为是稳定的,但我仍然希望您在 GitHub 上提出评论和贡献,或者在 Twitter 上向我提出问题:@thesumofall
关于 Florian Heinemann
Florian 是麻省理工学院的系统设计和管理研究员,专注于复杂的社会技术系统。在加入空中客车公司担任知识和创新管理经理之前,他曾在企业软件领域的几家初创公司工作。Florian 对网络、安全、解决复杂问题和冬季运动充满热情。
关于 Robert Nyman [荣誉编辑]
Mozilla Hacks 的技术布道师和编辑。发表有关 HTML5、JavaScript 和开放 Web 的演讲和博客文章。Robert 是 HTML5 和开放 Web 的坚定支持者,自 1999 年以来一直在从事 Web 前端开发工作,在瑞典和纽约市都有工作经历。他经常在http://robertnyman.com 上发表博客文章,并喜欢旅行和结识新朋友。
38 条评论