在 Node.JS 中实现无密码身份验证

2025-05-24

在 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>
Enter fullscreen mode Exit fullscreen mode

这就是 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;
  }
};
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

有了基础服务器,我们就可以开始添加更多功能了。我们先来添加要用到的电子邮件服务。首先,将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>
`
Enter fullscreen mode Exit fullscreen mode

接下来,我们需要创建用于保存用户信息的令牌。这只是令牌中可能包含的一些基本内容的示例。您还可以包含用户权限、特殊访问密钥以及其他可能在您的应用中使用的信息。

// 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);
};
Enter fullscreen mode Exit fullscreen mode

现在我们可以更新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}`);
    }
  });
});
Enter fullscreen mode Exit fullscreen mode

我们只需要在代码中添加两件事就可以完成服务器的搭建。首先添加一个account端点,然后添加一个简单的身份验证方法。

// Get account information
app.get("/account", (req, res) => {
  isAuthenticated(req, res)
});
Enter fullscreen mode Exit fullscreen mode

这将从前端获取用户的令牌并调用身份验证功能。

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.")
}
Enter fullscreen mode Exit fullscreen mode

此身份验证检查会从 URL 查询中获取用户的令牌,并尝试使用创建令牌时使用的密钥对其进行解码。如果解码失败,则会向前端返回一条错误消息。如果令牌解码成功,则会进行其他一些检查,最终用户身份验证通过,并可以访问应用!

现有身份验证系统的最佳实践

现有系统可能无法实现无密码身份验证,但您可以采取一些措施来使您的应用程序更安全。

  • 增加密码的复杂性要求。
  • 使用双因素身份验证。
  • 要求在一定时间后更改密码。

结论

有很多不同的方法可以为你的应用实现身份验证系统,无密码身份验证只是其中之一。基于令牌的身份验证是另一种常用的身份验证类型,有很多方法可以实现它。

构建自己的身份验证系统可能比您投入的时间更多。有很多现有的库和服务可用于将身份验证集成到您的应用中。其中一些最常用的库和服务是 Passport.js 和 Auth0。

文章来源:https://dev.to/flippedcoding/implementing-passwordless-authentication-in-node-js-43m0
PREV
函数式编程简介
NEXT
如何成为一名优秀的软件工程师导师