假设您正在构建一个即将发布的 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
为开发依赖项,以便在代码更改时自动重启服务器。
<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
文件。添加像我的代码一样的脚本并输入。
<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 上启动服务器,允许它监听传入的请求。
<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 对象,表示相应的端点已被命中。最后,我们将路由器实例导出为默认导出,使其可用于应用程序的其他部分。
<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
添加身份验证路由来测试您的端点。
<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。您可以像这样测试所有端点。
类似地,您可以看到其他路线,如Logout和SignUp。
所以,我们的基本应用程序已经准备就绪...现在使其成为一个强大且合适的身份验证系统。
使用 Mongoose Schema 创建用户模型
现在,首先准备好我们的 mongoDB 数据库。为此,创建一个文件夹 Model,并在其下创建一个文件User.js
,然后在此文件中添加 Mongoose 架构和 mongoDB 数据库中的模型User
。架构包括username
、fullName
、password
和 的字段email
,每个字段都具有指定的数据类型和约束,例如唯一性和必需状态。密码字段的最小长度也是 6 个字符。
<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 退出进程。该函数被导出以供应用程序的其他部分使用。
<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。
<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
<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 状态email
。password
接下来,该函数检查用户名或电子邮件是否已存在于数据库中。如果已存在,则返回带有错误消息的 400 状态。它还确保密码长度至少为 6 个字符,如果不满足此条件,则发送另一个 400 状态。
然后使用 安全地对密码进行散列bcrypt
。使用提供的数据创建一个新User
实例并将其保存到数据库。
保存后,该函数会生成一个 JWT 令牌,将其设置为 cookie,并返回 201 状态,其中包含用户的 ID、全名、用户名和电子邮件。如果发生任何错误,则会记录这些错误,并发送 500 状态和“内部服务器错误”消息。
要激活此功能,您必须导入这些
<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
。
<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
。您可以像这样添加任何类型的数字和字符串的混合。
<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
功能已经完成...所以现在就制定它的路线。
<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 :从安全地加载环境变量.env
;express.json():解析传入的 JSON 请求;express.urlencoded({extended:true}):解析 URL 编码的数据;cookieParser:处理 JWT 令牌的 cookie;connectMongoDB():连接到 MongoDB 进行数据存储;路线: /api/auth 管理注册、登录和注销。
这是更新的代码index.js
<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
文件
<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
<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 中测试一下。
在这里您可以看到它已成功显示已登录。
登出功能
好的。现在是最后一个函数,即注销函数。让我们来实现它。这很简单。
<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
<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