掌握 Node.js 中的安全身份验证:使用 bcrypt.js 和 JWT 登录/注销

假设您正在构建一个即将发布的 Web 应用程序。您精心设计了用户界面,添加了令人兴奋的功能,并确保一切运行顺利。但随着发布日期的临近,一个令人烦恼的问题开始让您担心------安全性。具体来说,如何确保只有正确的用户才能访问应用程序的正确部分。这就是身份验证的作用所在。

身份验证是验证用户身份的过程,也是 Web 开发的关键方面。在广阔的数字环境中,确保用户可以安全地登录和退出应用程序至关重要。一旦失误,您的应用程序就可能容易受到攻击,使用户数据面临风险。

介绍

在本文中,我们将探讨 Node.js 中的安全身份验证,使用bcrypt.js哈希密码和 JWT 令牌来管理用户会话。最后,您将对如何实施强大的登录/注销系统有深入的理解,从而确保用户数据的安全。

那么,让我们踏上构建万无一失的身份验证系统的旅程,从设置环境到使用 JWT 保护我们的路由。准备好锁定您的 Node.js 应用程序了吗?让我们开始吧。

设置Node.js项目环境

首先,使用 初始化您的 Node.js 项目npm init -y,这将创建一个package.json具有默认设置的文件。接下来,安装必要的软件包:express用于设置服务器、mongoose用于管理 MongoDB、jsonwebtoken用于处理 JWT 令牌、bcryptjs用于散列密码、dotenv用于环境变量、cors用于启用跨源资源共享、cookie-parser用于解析 cookie。最后,添加nodemon为开发依赖项,以便在代码更改时自动重启服务器。

highlight 复制代码
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code>1.`npm init -y`
2.`npm install express mongoose jsonwebtoken bcryptjs dotenv cors cookie-parser`
3.`npm install nodemon -D`

</code></span></span>

现在修改package.json文件。添加像我的代码一样的脚本并输入。

highlight 复制代码
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code>"scripts": {
    "dev": "nodemon backend/index.js",
    "start": "node backend/index.js"
  },
"type": "module",

</code></span></span>

基本服务器设置

接下来,我们将设置一个基本的 Express 服务器。创建一个名为 的文件index.js。此代码初始化 Express 并创建应用程序的实例。然后,我们将为根 URL("/")定义一个路由来处理传入的 HTTP GET 请求。之后,我们将在端口 8000 上启动服务器,允许它监听传入的请求。

highlight 复制代码
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code>import express from "express";
const app = express();

app.get("/", (req, res) => {
  res.send("Server is ready");
});

app.listen(8000, () => {
  console.log("Server is running on PORT 8000");
});
</code></span></span>

设置基本身份验证路由

现在,我们将创建一个名为"routes"的文件夹,并在该文件夹中创建一个名为的新文件authRoute.js并粘贴以下代码以查看路线的基础知识。

在此代码片段中,我们使用 Express 为不同的身份验证端点设置路由。首先,我们导入 express 库并创建一个新的路由器实例。然后,我们定义三个 GET 路由:/signup/login/logout,每个路由都响应一个 JSON 对象,表示相应的端点已被命中。最后,我们将路由器实例导出为默认导出,使其可用于应用程序的其他部分。

highlight 复制代码
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code>import express from "express";

// Create a new Express router instance
const router = express.Router();

// Define a GET route for the signup endpoint
router.get("/signup", (req, res) => {
  // Return a JSON response indicating that the signup endpoint was hit
  res.json({
    data: "You hit signup endpoint",
  });
});

// Define a GET route for the login endpoint
router.get("/login", (req, res) => {
  // Return a JSON response indicating that the login endpoint was hit
  res.json({
    data: "You hit login endpoint",
  });
});

// Define a GET route for the logout endpoint
router.get("/logout", (req, res) => {
  // Return a JSON response indicating that the logout endpoint was hit
  res.json({
    data: "You hit logout endpoint",
  });
});

// Export the router instance as the default export
export default router;
</code></span></span>

现在更改index.js添加身份验证路由来测试您的端点。

highlight 复制代码
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code>import express from "express";
import authRoute from "./routes/authRoutes.js";
const app = express();

app.get("/", (req, res) => {
  res.send("Server is ready");
});

app.use("/api/auth", authRoute);

app.listen(8000, () => {
  console.log("Server is running on PORT 8000");
});
</code></span></span>

现在,您可以在浏览器中进行测试...但为了方便起见,我将使用 Postman。您可以像这样测试所有端点。

类似地,您可以看到其他路线,如LogoutSignUp

所以,我们的基本应用程序已经准备就绪...现在使其成为一个强大且合适的身份验证系统。

使用 Mongoose Schema 创建用户模型

现在,首先准备好我们的 mongoDB 数据库。为此,创建一个文件夹 Model,并在其下创建一个文件User.js,然后在此文件中添加 Mongoose 架构和 mongoDB 数据库中的模型User。架构包括usernamefullNamepassword和 的字段email,每个字段都具有指定的数据类型和约束,例如唯一性和必需状态。密码字段的最小长度也是 6 个字符。

highlight 复制代码
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code>import mongoose from "mongoose";

// Define the User schema with various fields and their data types
const userSchema = new mongoose.Schema(
  {
    // The unique username of the user
    username: {
      type: String,
      required: true,
      unique: true,
    },
    fullName: {
      type: String,
      required: true,
    },
    // The password of the user (min length: 6)
    password: {
      type: String,
      required: true,
      minLength: 6,
    },
    // The email of the user (unique)
    email: {
      type: String,
      required: true,
      unique: true,
    },
  },
  { timestamps: true }
);

// Create the User model based on the userSchema
const User = mongoose.model("User", userSchema);

// Export the User model
export default User;
</code></span></span>

使用 Mongoose 连接到 MongoDB

现在让我们连接到数据库。我们将创建一个名为 db 的文件夹,并在其中创建一个名为 的文件connectDB.js。在此文件中,我们将定义一个异步函数connectMongoDB,该函数尝试使用 Mongoose 连接到 MongoDB 数据库。它从MONGO_URI环境变量中获取数据库连接字符串。如果连接成功,它会记录一条带有主机名的成功消息。如果失败,它会记录错误并以状态代码 1 退出进程。该函数被导出以供应用程序的其他部分使用。

highlight 复制代码
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code>import mongoose from "mongoose";

const connectMongoDB = async () => {
  try {
    // Use the mongoose.connect() method to connect to the database
    // using the MONGO_URI environment variable
    const conn = await mongoose.connect(process.env.MONGO_URI);
    console.log(`MongoDB connected: ${conn.connection.host}`);
  } catch (error) {
    console.error(`Error connection to mongoDB: ${error.message}`);
    process.exit(1);
  }
};

export default connectMongoDB;
</code></span></span>

现在要使用它,MONGO_URI我们必须在.env文件中创建它。在这里我使用了本地 mongoDB 设置连接字符串。如果您愿意,也可以使用 mongoDB atlas。

highlight 复制代码
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code>MONGO_URI=mongodb://localhost:27017/auth-test
</code></span></span>

注册功能

现在制作注册功能。为此,首先创建一个文件夹控制器,然后创建文件authController.js

highlight 复制代码
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code>export const signup = async (req, res) => {
  try {
    const { fullName, username, email, password } = req.body; // Destructuring user input

    // Regular expression for email format validation
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(email)) {
      return res.status(400).json({ error: "Invalid email format" });
    }

    // Checking if username is already taken
    const existingUser = await User.findOne({ username });
    if (existingUser) {
      return res.status(400).json({ error: "Username is already taken" });
    }

    // Checking if email is already taken
    const existingEmail = await User.findOne({ email });
    if (existingEmail) {
      return res.status(400).json({ error: "Email is already taken" });
    }

    // Validating password length
    if (password.length < 6) {
      return res
        .status(400)
        .json({ error: "Password must be at least 6 characters long" });
    }

    // Generating salt and hashing the password
    const salt = await bcrypt.genSalt(10);
    const hashedPassword = await bcrypt.hash(password, salt);

    // Creating a new user instance
    const newUser = new User({
      fullName,
      username,
      email,
      password: hashedPassword,
    });

    // Saving the new user to the database
    await newUser.save();

    // Generating JWT token and setting it as a cookie
    generateTokenAndSetCookie(newUser._id, res);

    // Sending success response with user data
    res.status(201).json({
      _id: newUser._id,
      fullName: newUser.fullName,
      username: newUser.username,
      email: newUser.email,
    });
  } catch (error) {
    // Handling errors
    console.log("Error in signup controller", error.message);
    res.status(500).json({ error: "Internal Server Error" });
  }
};
</code></span></span>

首先,它从请求正文中提取fullName、、username和。它使用正则表达式验证电子邮件格式,如果格式无效,则返回 400 状态emailpassword

接下来,该函数检查用户名或电子邮件是否已存在于数据库中。如果已存在,则返回带有错误消息的 400 状态。它还确保密码长度至少为 6 个字符,如果不满足此条件,则发送另一个 400 状态。

然后使用 安全地对密码进行散列bcrypt。使用提供的数据创建一个新User实例并将其保存到数据库。

保存后,该函数会生成一个 JWT 令牌,将其设置为 cookie,并返回 201 状态,其中包含用户的 ID、全名、用户名和电子邮件。如果发生任何错误,则会记录这些错误,并发送 500 状态和"内部服务器错误"消息。

要激活此功能,您必须导入这些

highlight 复制代码
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code>import { generateTokenAndSetCookie } from "../utils/generateToken.js";
import User from "../Model/User.js";
import bcrypt from "bcryptjs";
</code></span></span>

注意到了什么吗?一个新东西叫做generateTokenAndSetCookie ...让我们看看它的代码...创建一个文件夹utils然后在那里generateTokenAndSetCookie.js

highlight 复制代码
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code>// Import the jsonwebtoken library
import jwt from "jsonwebtoken";

// Define a function that generates a JWT and sets it as a cookie
export const generateTokenAndSetCookie = (userId, res) => {
  // Use the jsonwebtoken library to sign a JWT with the user ID and a secret key
  // Set the expiration time of the token to 15 days
  const token = jwt.sign({ userId }, process.env.JWT_SECRET, {
    expiresIn: "15d",
  });
  res.cookie("jwt", token, {
    maxAge: 15 * 24 * 60 * 60 * 1000, //MS
    httpOnly: true, // prevent XSS attacks cross-site scripting attacks
    sameSite: "strict", // CSRF attacks cross-site request forgery attacks
    secure: process.env.NODE_ENV !== "development",
  });
};
</code></span></span>

**generateTokenAndSetCookie**函数创建一个JWT并将其存储在cookie中,用于用户身份验证。

JWT 生成:

该函数使用jsonwebtoken库创建 JWT。它使用用户的 ID 和密钥(JWT_SECRET来自环境变量)对令牌进行签名,并将其设置为 15 天后过期。

设置Cookie:

然后,令牌将存储在用户浏览器的 Cookie 中。Cookie 配置了几个安全属性:

  • maxAge:将 cookie 的寿命设置为 15 天。
  • httpOnly:确保无法通过 JavaScript 访问 cookie,从而防止 XSS(跨站点脚本)攻击。
  • sameSite:"strict":通过限制仅与来自同一站点的请求一起发送 cookie 来防止 CSRF(跨站点请求伪造)攻击。
  • 安全:确保仅在环境不是开发时通过 HTTPS 发送 cookie,从而增加额外的安全性。

因此,此功能可确保用户会话既安全又持久,使其成为身份验证过程的关键部分。

JWT_SECRET这里我们必须添加另一个环境变量.env。您可以像这样添加任何类型的数字和字符串的混合。

highlight 复制代码
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code>MONGO_URI=mongodb://localhost:27017/auth-test
JWT_SECRET=GBfChCMY8MB7wnymkWhtoD3cRlA1CC5e6iWSBmnuaSg
</code></span></span>

现在我们的signUp功能已经完成...所以现在就制定它的路线。

highlight 复制代码
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code>import express from "express";
import { signup } from "../controllers/authController.js";

const router = express.Router();

router.post("/signup", signup);

export default router;
</code></span></span>

好的,现在让我们修改我们的index.js这里我们添加了一些新的导入。dotenv 从安全地加载环境变量.envexpress.json(): 解析传入的 JSON 请求;express.urlencoded({extended:true}): 解析 URL 编码的数据;cookieParser: 处理 JWT 令牌的 cookie;connectMongoDB(): 连接到 MongoDB 进行数据存储;路线: /api/auth 管理注册、登录和注销。

这是更新的代码index.js

highlight 复制代码
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code>import express from "express";
import dotenv from "dotenv";
import authRoute from "./routes/authRoutes.js";
import connectMongoDB from "./db/connectDB.js";
import cookieParser from "cookie-parser";

dotenv.config();

const app = express();

app.get("/", (req, res) => {
  res.send("Server is ready");
});

// Middleware to parse JSON request bodies
app.use(express.json()); // for parsing application/json

// Middleware to parse URL-encoded request bodies (if you're using form data)
app.use(express.urlencoded({ extended: true }));

app.use(cookieParser());

app.use("/api/auth", authRoute);

app.listen(8000, () => {
  console.log("Server is running on PORT 8000");
  connectMongoDB();
});
</code></span></span>

那么,现在是时候在 Postman 中测试我们的注册功能了。让我们看看它是否正常工作。

因此,结果如下。

在这里您可以看到它运行正常,并且您也可以检查您的 mongoDB 数据库。

登录功能

现在制作登录函数。让我们再次回到我们的authController.js文件

highlight 复制代码
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code>export const login = async (req, res) => {
  try {
    const { username, password } = req.body; // Destructuring user input

    // Finding the user by username
    const user = await User.findOne({ username });

    // Comparing passwords using bcrypt
    const isPasswordCorrect = await bcrypt.compare(
      password,
      user?.password || ""
    );

    // If user or password is incorrect, return error
    if (!user || !isPasswordCorrect) {
      return res.status(400).json({ error: "Invalid username or password" });
    }

    // Generating JWT token and setting it as a cookie
    generateTokenAndSetCookie(user._id, res);

    // Sending success response with user data
    res.status(200).json({
      message: "Logged in successfully",
    });
  } catch (error) {
    // Handling errors
    console.log("Error in login controller", error.message);
    res.status(500).json({ error: "Internal Server Error" });
  }
};
</code></span></span>

登录控制器通过验证用户名和密码来验证用户身份。它首先使用用户名在数据库中搜索用户。如果找到,它会使用 bcrypt 将提供的密码与存储在数据库中的哈希密码进行比较。如果用户名或密码不正确,它会返回错误响应。验证成功后,它会生成一个 JWT 令牌,使用 将其设置为 cookie generateTokenAndSetCookie,并以成功消息响应,表明用户已成功登录。

让我们添加登录路线authRoutes.js

highlight 复制代码
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code>import express from "express";
import { login, signup } from "../controllers/authController.js";

const router = express.Router();

router.post("/signup", signup);
router.post("/login", login);

export default router;
</code></span></span>

让我们在 Postman 中测试一下。

在这里您可以看到它已成功显示已登录。

登出功能

好的。现在是最后一个函数,即注销函数。让我们来实现它。这很简单。

highlight 复制代码
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code>export const logout = async (req, res) => {
  try {
    // Clearing JWT cookie
    res.cookie("jwt", "", { maxAge: 0 });
    // Sending success response
    res.status(200).json({ message: "Logged out successfully" });
  } catch (error) {
    // Handling errors
    console.log("Error in logout controller", error.message);
    res.status(500).json({ error: "Internal Server Error" });
  }
};
</code></span></span>

注销控制器通过使用 清除客户端浏览器中的 JWT cookie res.cookie,将其值设置为空字符串并将其maxAge设置为 0,以确保立即过期,从而安全地注销用户。成功清除 cookie 后,它会发送成功响应,其中包含一条消息,表明用户已成功注销。如果在此过程中发生任何错误,它会捕获错误、记录错误并返回内部服务器错误响应。

将此路线添加到我们的authRoute.js

highlight 复制代码
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code>import express from "express";
import { login, logout, signup } from "../controllers/authController.js";

const router = express.Router();

router.post("/signup", signup);
router.post("/login", login);
router.post("/logout", logout);

export default router;
</code></span></span>

好的。让我们测试一下我们的最后一个功能,看看它是否运行正常。

哦!......它工作得非常好。😃😃

现在,我们这个身份验证的完整后端已经准备好了。🎉🎉

示例源码我已上传,点击链接自行下载:https://download.csdn.net/download/wangliang6212/90259260

相关推荐
老朋友此林3 分钟前
React Hook原理速通笔记1(useEffect 原理、使用踩坑、渲染周期、依赖项)
javascript·笔记·react.js
克里斯蒂亚诺更新4 分钟前
vue3使用pinia替代vuex举例
前端·javascript·vue.js
sam.li9 分钟前
鸿蒙HAR对外发布安全流程
安全·华为·harmonyos
sam.li19 分钟前
鸿蒙APP安全体系
安全·华为·harmonyos
冰暮流星20 分钟前
javascript赋值运算符
开发语言·javascript·ecmascript
寧笙(Lycode)27 分钟前
前端包管理工具——npm、yarn、pnpm详解
前端·npm·node.js
Ai野生菌36 分钟前
论文解读 | 当“提示词”学会绕路:用拓扑学方法一次击穿多智能体安全防线
人工智能·深度学习·安全·语言模型·拓扑学
遗憾随她而去.1 小时前
Webpack5 基础篇(二)
前端·webpack·node.js
德迅云安全-小娜1 小时前
主机安全功能:主机的风险与监测
网络·安全
小快说网安1 小时前
等保测评通过后,如何持续满足安全运维要求?
运维·安全·网络安全·等保测评