在 Node.JS 中实现无密码身份验证
身份验证失效是Web 应用程序的第二大安全风险。这通常意味着会话管理和身份验证处理不当。这为攻击者提供了多种途径来获取可恶意使用的数据。
因此,务必在开发过程中尽早落实最佳实践。您可以采取一些措施来提升身份验证流程的安全性,保护用户安全。我们将通过一个 Node.js 应用来演示其中的一些措施。
首先,让我们了解一下处理身份验证的一些不同方法。
身份验证方法
有几种不同类型的身份验证方法可供选择:基于会话的身份验证、基于令牌的身份验证和无密码身份验证。每种身份验证方法都有其优缺点,我们将介绍其中几种。
基于会话的身份验证
这是最常见的身份验证形式。它只需要与数据库中的用户名和密码匹配即可。如果用户输入了正确的登录凭据,系统会为他们初始化一个带有特定 ID 的会话。会话通常在用户退出应用时结束。
如果会话正确实现,它们会在设定的时间后自动过期。你会在金融应用中经常看到这种功能,例如银行和交易。这为用户提供了额外的安全保障,以防他们在公共电脑上登录银行账户后忘记了该标签页。
基于令牌的身份验证
基于令牌的身份验证不使用实际凭据来验证请求,而是向用户提供存储在浏览器中的临时令牌。此令牌通常是一个JWT(JSON Web 令牌),其中包含端点验证用户所需的所有信息。
用户发出的每个请求都会包含该令牌。使用令牌的好处之一是,它可以嵌入用户可能拥有的角色和权限信息,而无需从数据库获取这些数据。这使得攻击者即使能够窃取用户的令牌,也能减少对关键信息的访问。
无密码身份验证
这种身份验证方式与其他方式截然不同。登录无需任何凭证。您只需要一个与账户关联的电子邮件地址或电话号码,每次登录时都会收到一个魔术链接或一次性密码。点击链接后,您将被重定向到应用程序,并且已经登录。之后,魔术链接将失效,其他人无法使用它。
生成魔法链接时,JWT 也会随之生成。身份验证就是这样进行的。使用这种登录方式,攻击者入侵系统的难度大大增加。他们可以利用的输入更少,而且通过魔法链接发送 JWT 比通过响应发送更难被拦截。
现在您已经了解了这些不同的身份验证方法,让我们实现无密码身份验证模型。
在 Node 中实现身份验证
无密码身份验证流程
我们将首先介绍无密码身份验证的流程。
- 用户在网络应用程序中提交他们的电子邮件地址或电话号码。
- 他们收到了一个用于登录的魔术链接。
- 用户点击魔术链接后,将被重定向到应用程序,并且已经登录。
现在我们有了需要实现的流程,让我们从制作一个超级基本的前端开始。
前端设置
由于我们主要关注的是后端,所以我们甚至不需要使用 JavaScript 框架。所以我们将使用一些基本的 HTML 和 JavaScript 来制作前端。
用户界面代码如下。只是一个使用 frontend.js 文件的小型 HTML 文件。
<!DOCTYPE html>
<html>
<head>
<title>Passwordless Authentication</title>
<script src="./frontend.js"></script>
</head>
<body>
<h1>This is where you'll put your email to get a magic link.</h1>
<form>
<div>
<label for="email_address">Enter your email address</label>
<input type="email" id="email_address" />
</div>
<button type="submit" id="submit_email">Get magic link</button>
</form>
</body>
</html>
这就是 frontend.js 文件的样子。
window.onload = () => {
const submitButton = document.getElementById("submit_email");
const emailInput = document.getElementById("email_address")
submitButton.addEventListener("click", handleAuth);
/** This function submits the request to the server for sending the user a magic link.
* Params: email address
* Returns: message
*/
async function handleAuth() {
const message = await axios.post("http://localhost:4300/login", {
email: emailInput.value
});
return message;
}
};
JavaScript 文件获取我们在 HTML 文件中创建的提交按钮,并为其添加一个点击事件监听器。因此,当按钮被点击时,我们将向http://localhost:4300
终端上运行的服务器发送一个 POST 请求login
,并传入输入的电子邮件地址。然后,如果 POST 请求成功,我们将收到一条可以显示给用户的消息。
后端设置
现在我们要开始制作 Node 应用了。我们先创建一个 Express 应用,然后安装一些软件包。
import cors from "cors";
import express from "express";
const PORT = process.env.PORT || 4000;
const app = express();
// Set up middleware
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
// Login endpoint
app.post("/login", (req, res) => {
const email = req.body.email;
if (!email) {
res.statusCode(403);
res.send({
message: "There is no email address that matches this.",
});
}
if (email) {
res.statusCode(200);
res.send(email);
}
});
// Start up the server on the port defined in the environment
const server = app.listen(PORT, () => {
console.info("Server running on port " + PORT)
})
export default server
有了基础服务器,我们就可以开始添加更多功能了。我们先来添加要用到的电子邮件服务。首先,将nodemailer添加到 package.json 文件中,然后导入它。
import nodeMailer from "nodemailer";
然后在中间件下方,我们将创建一个传输器来发送电子邮件。这段代码配置了Nodemailer,并用一些简单的 HTML 代码创建了电子邮件模板。
// Set up email
const transport = nodeMailer.createTransport({
host: process.env.EMAIL_HOST,
port: 587,
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASSWORD
}
});
// Make email template for magic link
const emailTemplate = ({ username, link }) => `
<h2>Hey ${username}</h2>
<p>Here's the login link you just requested:</p>
<p>${link}</p>
`
接下来,我们需要创建用于保存用户信息的令牌。这只是令牌中可能包含的一些基本内容的示例。您还可以包含用户权限、特殊访问密钥以及其他可能在您的应用中使用的信息。
// Generate token
const makeToken = (email) => {
const expirationDate = new Date();
expirationDate.setHours(new Date().getHours() + 1);
return jwt.sign({ email, expirationDate }, process.env.JWT_SECRET_KEY);
};
现在我们可以更新login
端点,向注册用户发送一个魔术链接,他们点击该链接后就会立即登录到应用程序。
// Login endpoint
app.post("/login", (req, res) => {
const { email } = req.body;
if (!email) {
res.status(404);
res.send({
message: "You didn't enter a valid email address.",
});
}
const token = makeToken(email);
const mailOptions = {
from: "You Know",
html: emailTemplate({
email,
link: `http://localhost:8080/account?token=${token}`,
}),
subject: "Your Magic Link",
to: email,
};
return transport.sendMail(mailOptions, (error) => {
if (error) {
res.status(404);
res.send("Can't send email.");
} else {
res.status(200);
res.send(`Magic link sent. : http://localhost:8080/account?token=${token}`);
}
});
});
我们只需要在代码中添加两件事就可以完成服务器的搭建。首先添加一个account
端点,然后添加一个简单的身份验证方法。
// Get account information
app.get("/account", (req, res) => {
isAuthenticated(req, res)
});
这将从前端获取用户的令牌并调用身份验证功能。
const isAuthenticated = (req, res) => { const { token } = req.query
if (!token) {
res.status(403)
res.send("Can't verify user.")
return
}
let decoded
try {
decoded = jwt.verify(token, process.env.JWT_SECRET_KEY)
}
catch {
res.status(403)
res.send("Invalid auth credentials.")
return
}
if (!decoded.hasOwnProperty("email") || !decoded.hasOwnProperty("expirationDate")) {
res.status(403)
res.send("Invalid auth credentials.")
return
}
const { expirationDate } = decoded
if (expirationDate < new Date()) {
res.status(403)
res.send("Token has expired.")
return
}
res.status(200)
res.send("User has been validated.")
}
此身份验证检查会从 URL 查询中获取用户的令牌,并尝试使用创建令牌时使用的密钥对其进行解码。如果解码失败,则会向前端返回一条错误消息。如果令牌解码成功,则会进行其他一些检查,最终用户身份验证通过,并可以访问应用!
现有身份验证系统的最佳实践
现有系统可能无法实现无密码身份验证,但您可以采取一些措施来使您的应用程序更安全。
- 增加密码的复杂性要求。
- 使用双因素身份验证。
- 要求在一定时间后更改密码。
结论
有很多不同的方法可以为你的应用实现身份验证系统,无密码身份验证只是其中之一。基于令牌的身份验证是另一种常用的身份验证类型,有很多方法可以实现它。
构建自己的身份验证系统可能比您投入的时间更多。有很多现有的库和服务可用于将身份验证集成到您的应用中。其中一些最常用的库和服务是 Passport.js 和 Auth0。
文章来源:https://dev.to/flippedcoding/implementing-passwordless-authentication-in-node-js-43m0