【Express+TS手写对象存储】第3章:用户管理模块(JWT、RBAC)
欢迎阅读手写Express+TS手写对象存储
专栏。这系列文章旨在为您提供一个全面的指南,带您一步步构建一个功能齐全的对象存储系统。对象存储系统在大数据时代发挥着重要的作用,能够高效地管理和存储大量非结构化数据,如图片、视频和文档等。
这是专栏中的第三节,本文将带您深入用户管理模块的开发。我们将一步步实现用户注册与登录功能,使用JWT进行用户认证与授权,并通过RBAC实现细粒度的角色与权限管理。此外,我们还将介绍如何编写单元测试与集成测试,以确保用户管理模块的稳定性和可靠性。
接口设计文档
Auth 接口设计文档
名称 | 地址 | 方法 | 功能说明 | 角色 | 是否要求登录 |
---|---|---|---|---|---|
用户注册 | /api/auth/register | POST | 注册新用户 | 任何用户 | 否 |
用户登录 | /api/auth/login | POST | 用户登录 | 任何用户 | 否 |
User 接口设计文档
名称 | 地址 | 方法 | 功能说明 | 角色 | 是否要求登录 |
---|---|---|---|---|---|
获取用户信息 | /api/user/profile | GET | 获取当前用户信息 | 已登录用户 | 是 |
更新用户信息 | /api/user/profile | PUT | 更新当前用户信息 | 已登录用户 | 是 |
修改密码 | /api/user/reset/password | PUT | 修改当前用户密码 | 已登录用户 | 是 |
用户退出登录 | /api/user/logout | POST | 用户退出登录 | 已登录用户 | 是 |
升级用户为管理员 | /api/user/upgrade2admin | PUT | 升级普通用户为管理员 | 管理员 | 是 |
接口实现
3.1 用户注册与登录功能
3.1.1 创建用户模型
-
安装必要的库:
npm install bcryptjs npm i --save-dev @types/bcryptjs
-
定义用户Schema,使用bcrypt进行密码加密:
在
models
目录下创建UserModel.ts
文件:import mongoose, { Schema, Document } from "mongoose"; import bcrypt from "bcryptjs"; // 用户接口,包含字段和方法 export interface IUser extends Document { username: string; email: string; password: string; comparePassword(candidatePassword: string): Promise<boolean>; } // 用户 Schema 定义 const UserSchema: Schema = new Schema({ username: { type: String, required: true, unique: true }, email: { type: String, required: true, unique: true }, password: { type: String, required: true }, }); // 保存用户前加密密码 UserSchema.pre<IUser>("save", async function (next) { if (!this.isModified("password")) return next(); const salt = await bcrypt.genSalt(10); const hash = await bcrypt.hash(this.password, salt); this.password = hash; next(); }); // 比较密码方法 UserSchema.methods.comparePassword = function ( candidatePassword: string ): Promise<boolean> { return bcrypt.compare(candidatePassword, this.password); }; export default mongoose.model<IUser>("User", UserSchema);
3.1.2 实现注册和登录接口
-
创建注册和登录的路由和控制器:
在
controllers
目录下创建authController.ts
文件:import { Request, Response } from "express"; import UserModel from "../models/UserModel"; export const register = async (req: Request, res: Response) => { const { username, email, password } = req.body; try { const user = new UserModel({ username, email, password }); await user.save(); res.status(201).json({ message: "User registered successfully" }); } catch (error) { res.status(400).json({ message: 'User already exists' }); } }; export const login = async (req: Request, res: Response) => { const { email, password } = req.body; try { const user = await UserModel.findOne({ email }); if (!user) { return res.status(400).json({ error: "Invalid credentials" }); } const isMatch = await user.comparePassword(password); if (!isMatch) { return res.status(400).json({ error: "Invalid credentials" }); } // Generate JWT token here (to be implemented in 3.2) res.status(200).json({ message: "Login successful" }); } catch (error) { res.status(500).json({ error: "Server error" }); } };
在
routes
目录下创建authRoutes.ts
文件:import { Router } from "express"; import { register, login } from "../controllers/authController"; const router = Router(); router.post("/register", register); router.post("/login", login); export default router;
-
在主应用文件中使用路由:
在
app.ts
文件中添加路由:import express from "express"; import cors from "cors"; import morgan from "morgan"; import helloRoute from "./routes/helloRoute"; import { initializeMongoose } from "./dao/mongodb"; import authRoutes from "./routes/authRoutes"; const app = express(); const port = process.env.PORT || 3000; // 初始化 initializeMongoose(); // 配置中间件 app.use(express.json()); // 使用 Express 内置的 JSON 解析中间件 app.use(express.urlencoded({ extended: true })); // 使用 Express 内置的 URL 编码解析中间件 app.use(cors()); app.use(morgan("dev")); // 配置路由 app.use("/api", helloRoute); app.use("/api/auth", authRoutes); app.get("/", (req, res) => { res.send("Hello, Object Storage System!"); }); app.listen(port, () => { console.log(`Server is running on http://localhost:${port}`); });
3.2 使用JWT进行用户认证与授权
3.2.1 解释JWT的工作原理
- JWT结构:JWT由三个部分组成:Header、Payload和Signature。
- 生成过程:服务器生成JWT时,会对Header和Payload进行Base64编码,并使用Secret进行签名。
- 验证过程:客户端发送请求时,会携带JWT,服务器验证签名和Payload的有效性。
3.2.2 生成和验证JWT的示例代码
-
安装JWT相关的库:
npm install jsonwebtoken npm i --save-dev @types/jsonwebtoken
-
生成和验证JWT:
在
controllers/authController.ts
文件中更新login
方法,生成JWT:import jwt from 'jsonwebtoken'; export const login = async (req: Request, res: Response) => { const { email, password } = req.body; try { const user = await UserModel.findOne({ email }); if (!user) { return res.status(400).json({ error: "Invalid credentials" }); } const isMatch = await user.comparePassword(password); if (!isMatch) { return res.status(400).json({ error: "Invalid credentials" }); } const token = jwt.sign( { userId: user._id, role: user.role }, "your_jwt_secret", { expiresIn: "1h", } ); res.setHeader("token", token); res.status(200).json({ message: "Login successful" }); } catch (error) { res.status(500).json({ error: "Server error" }); } };
-
编写JWT验证中间件:
在
middlewares
目录下创建authMiddleware.ts
文件:/* eslint-disable @typescript-eslint/no-namespace */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Request, Response, NextFunction } from "express"; import jwt from "jsonwebtoken"; declare global { namespace Express { interface Request { user?: { userId: string; role: "admin" | "user"; }; } } } export const authMiddleware = ( req: Request, res: Response, next: NextFunction ) => { const token = req.header("Authorization")?.replace("Bearer ", ""); if (!token) { return res.status(401).json({ error: "No token, authorization denied" }); } try { const decoded: any = jwt.verify(token, "your_jwt_secret"); req.user = decoded; next(); } catch (error) { res.status(401).json({ error: "Token is not valid" }); } };
-
保护API路由:
在需要保护的路由
src/routes/helloRoute.ts
中使用authMiddleware
:import { Router } from "express"; import { helloController } from "../controllers/helloController"; import { authMiddleware } from "../middlewares/authMiddleware"; const router = Router(); router.get("/hello", authMiddleware, helloController); export default router;
这次我们再访问http://localhost:3000/api/hello
就会拿到一个错误提示
{
"error":"No token, authorization denied"
}
3.3 用户角色与权限管理
3.3.1 RBAC的实现
-
什么是RBAC:
RBAC(基于角色的访问控制,Role-Based Access Control)是一种访问控制方法,用于管理用户对计算机系统资源的权限。它通过将权限分配给角色,然后将角色分配给用户来实现权限管理。这样,用户的权限是通过其所属的角色间接获得的,而不是直接赋予用户。
RBAC的主要优点包括:
- 简化权限管理:通过角色管理权限,而不是直接管理每个用户的权限,可以减少管理的复杂性。
- 提高安全性:通过严格定义角色和权限,可以更精细地控制用户对资源的访问。
- 灵活性和可扩展性:可以根据组织的需要动态调整角色和权限,适应不同的业务需求。
RBAC的基本概念包括:
- 用户(User):系统的实际使用者。
- 角色(Role):一组权限的集合,代表某一类用户的职责和权限。
- 权限(Permission):对系统资源的访问权,通常包括读、写、执行等操作。
- 会话(Session):用户在特定时间段内激活的角色。
RBAC的实施通常包括以下步骤:
- 定义角色:根据组织的业务需求和职能,定义不同的角色。
- 分配权限:将相应的权限分配给每个角色。
- 分配角色:将用户分配到相应的角色。
- 管理和维护:定期审查和更新角色、权限和用户的分配,以确保系统的安全性和有效性。
-
定义角色和权限:
更新
UserModel.ts
模型,添加角色字段:const UserSchema: Schema = new Schema({ username: { type: String, required: true, unique: true }, email: { type: String, required: true, unique: true }, password: { type: String, required: true }, role: { type: String, enum: ['user', 'admin'], default: 'user' } });
-
编写中间件进行权限控制:
在
middlewares
目录下创建roleMiddleware.ts
文件:import { Request, Response, NextFunction } from "express"; export const roleMiddleware = (requiredRole: string) => { return (req: Request, res: Response, next: NextFunction) => { if (!req.user || req.user.role !== requiredRole) { return res.status(403).json({ error: "Access denied" }); } next(); }; };
-
保护API路由:
在需要权限控制的路由中使用
roleMiddleware
:import { Router } from "express"; import { helloController } from "../controllers/helloController"; import { authMiddleware } from "../middlewares/authMiddleware"; import { roleMiddleware } from "../middlewares/roleMiddleware"; const router = Router(); router.get("/hello", authMiddleware, roleMiddleware("user"), helloController); router.get("/hello/admin", authMiddleware, roleMiddleware("admin"), helloController); export default router;
authMiddleware, roleMiddleware("admin")互换位置后访问
http://localhost:3000/api/hello
就能看到角色中间件的调用了{"error":"Access denied"}
3.4 用户信息管理
3.4.1 获取用户信息
-
编写获取用户信息的控制器:
在
controllers
目录下创建userController.ts
文件:import { Request, Response } from "express"; import UserModel from "../models/UserModel"; export const getUserProfile = async (req: Request, res: Response) => { try { const user = await UserModel.findById(req.user?.userId).select("-password"); if (!user) { return res.status(404).json({ error: "User not found" }); } res.status(200).json(user); } catch (error) { res.status(500).json({ error: "Server error" }); } };
-
创建获取用户信息的路由:
在
routes
目录下创建userRoutes.ts
文件:import { Router } from "express"; import { getUserProfile } from "../controllers/userController"; import { authMiddleware } from "../middlewares/authMiddleware"; const router = Router(); router.get("/profile", authMiddleware, getUserProfile); export default router;
-
在主应用文件中添加用户路由:
在
app.ts
文件中添加路由:import userRoutes from "./routes/userRoutes"; app.use("/api/user", userRoutes);
3.4.2 更新用户信息
-
编写更新用户信息的控制器:
在
controllers/userController.ts
文件中添加updateUserProfile
方法:export const updateUserProfile = async (req: Request, res: Response) => { const { username, email } = req.body; try { const user = await UserModel.findById(req.user?.userId); if (!user) { return res.status(404).json({ error: "User not found" }); } user.username = username || user.username; user.email = email || user.email; await user.save(); res.status(200).json({ message: "User updated successfully", user }); } catch (error) { res.status(500).json({ error: "Server error" }); } };
-
创建更新用户信息的路由:
在
routes/userRoutes.ts
文件中添加更新用户信息的路由:router.put("/profile", authMiddleware, updateUserProfile);
3.5 用户密码管理
3.5.1 修改密码功能
-
编写修改密码的控制器:
在
controllers/userController.ts
文件中添加changePassword
方法:export const changePassword = async (req: Request, res: Response) => { const { currentPassword, newPassword } = req.body; try { const user = await UserModel.findById(req.user?.userId); if (!user) { return res.status(404).json({ error: "User not found" }); } const isMatch = await user.comparePassword(currentPassword); if (!isMatch) { return res.status(400).json({ error: "Current password is incorrect" }); } user.password = newPassword; await user.save(); res.status(200).json({ message: "Password changed successfully" }); } catch (error) { res.status(500).json({ error: "Server error" }); } };
-
创建修改密码的路由:
在
routes/userRoutes.ts
文件中添加修改密码的路由:router.put("/reset/password", authMiddleware, changePassword);
3.6 用户退出登录功能
3.6.1 退出登录
-
编写用户注销的控制器:
在
controllers/userController.ts
文件中添加logout
方法:export const logout = async (req: Request, res: Response) => { // 清除客户端保存的JWT Token res.status(200).json({ message: "User logged out successfully" }); };
-
创建用户注销的路由:
在
routes/userRoutes.ts
文件中添加用户注销的路由:router.post("/logout", authMiddleware, logout);
3.8 创建管理员脚本
3.8.1 创建脚本文件
-
在
src/models/UserModel.ts
中扩展一下角色分类,新增super类// 用户 Schema 定义 const UserSchema: Schema = new Schema({ username: { type: String, required: true, unique: true }, email: { type: String, required: true, unique: true }, password: { type: String, required: true }, role: { type: String, enum: ["user", "admin","super"], default: "user" }, });
-
在
src
目录下创建一个scripts
文件夹,并在其中创建一个createAdmin.ts
文件:
import mongoose from "mongoose";
import readline from "readline";
import UserModel from "../models/UserModel";
import { initializeMongoose } from "../dao/mongodb";
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const question = (query: string) =>
new Promise<string>((resolve) => rl.question(query, resolve));
const createAdmin = async () => {
try {
await initializeMongoose();
const username = await question("Enter username: ");
const email = await question("Enter email: ");
const password = await question("Enter password: ");
const admin = new UserModel({
username,
email,
password: password,
role: "super",
});
await admin.save();
console.log("Admin user created successfully");
} catch (error) {
console.error("Error creating admin user:", error);
} finally {
rl.close();
mongoose.connection.close();
}
};
createAdmin();
3.8.2 更新 package.json
在 package.json
文件中添加一个脚本命令:
"scripts": {
"ca": "ts-node src/scripts/createAdmin.ts"
}
这样,运行 npm run ca
就会启动这个脚本,并提示用户输入用户名、邮箱和密码来创建管理员用户���
npm run ca
> ore@0.0.1 ca
> ts-node src/scripts/createAdmin.ts
MongoDB 连接成功...
Enter username: ar8d2336
Enter email: ar8d2336@163.com
Enter password: 123456
Admin user created successfully
Mongoose 已断开与数据库的连接
3.9 管理员升级普通用户为管理员的接口
3.9.1 编写控制器
在 controllers/userController.ts
文件中添加 upgradeToAdmin
方法:
export const upgradeToAdmin = async (req: Request, res: Response) => {
const { userId } = req.body;
try {
const user = await UserModel.findById(userId);
if (!user) {
return res.status(404).json({ error: "User not found" });
}
user.role = "admin";
await user.save();
res.status(200).json({ message: "User upgraded to admin successfully" });
} catch (error) {
res.status(500).json({ error: "Server error" });
}
};
3.9.2 扩展角色和抽象用户相关类型注解
在 src/types/user.d.ts
文件中添加 用户相关类型注解
:
export type Role = "admin" | "user" | "super";
export interface AuthUser {
userId: string;
role: Role;
}
3.9.3 改造auth中间件
在 src/middlewares/authMiddleware.ts
引入AuthUser注解并且改造中间件
/* eslint-disable @typescript-eslint/no-namespace */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";
import { AuthUser } from "../types/user";
declare global {
namespace Express {
interface Request {
user?: AuthUser; // --------------------------------这一行
}
}
}
export const authMiddleware = (
req: Request,
res: Response,
next: NextFunction
) => {
const token = req.header("Authorization")?.replace("Bearer ", "");
if (!token) {
return res.status(401).json({ error: "No token, authorization denied" });
}
try {
const decoded: any = jwt.verify(token, "your_jwt_secret");
req.user = decoded;
next();
} catch (error) {
res.status(401).json({ error: "Token is not valid" });
}
};
更新角色鉴权中间件src/middlewares/roleMiddleware.ts
,新增数组权限
import { Request, Response, NextFunction } from "express";
import { Role } from "../types/user";
export const roleMiddleware = (requiredRole: Role | Role[]) => {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(403).json({ error: "Access denied" });
}
if (typeof requiredRole === "string") {
if (req.user.role !== requiredRole) {
return res.status(403).json({ error: "Access denied" });
}
} else if (Array.isArray(requiredRole)) {
if (!requiredRole.includes(req.user.role)) {
return res.status(403).json({ error: "Access denied" });
}
}
next();
};
};
3.9.4 创建接口路由
在 routes/userRoutes.ts
文件中添加升级用户为管理员的路由:
import { Router } from "express";
import {
changePassword,
getUserProfile,
logout,
updateUserProfile,
upgradeToAdmin,
} from "../controllers/userController";
import { authMiddleware } from "../middlewares/authMiddleware";
import { roleMiddleware } from "../middlewares/roleMiddleware";
const router = Router();
router.get(
"/profile",
authMiddleware,
roleMiddleware(["admin", "super", "user"]),
getUserProfile
);
router.put(
"/profile",
authMiddleware,
roleMiddleware(["admin", "super", "user"]),
updateUserProfile
);
router.put(
"/reset/password",
authMiddleware,
roleMiddleware(["admin", "super", "user"]),
changePassword
);
router.post(
"/logout",
authMiddleware,
roleMiddleware(["admin", "super", "user"]),
logout
);
router.put(
"/upgrade2admin",
authMiddleware,
roleMiddleware("super"),
upgradeToAdmin
);
export default router;
测试
用户管理模块:集成测试用例
正向功能用例
-
用户注册功能
- 用例描述:用户可以通过提供用户名、邮箱和密码进行注册。
- 前置条件:无。
- 测试步骤:
- 发送POST请求到
/api/auth/register
,提供有效的用户名、邮箱和密码。 - 验证响应状态码为201。
- 验证响应消息为"User registered successfully"。
- 发送POST请求到
- 预期结果:用户注册成功,返回201状态码和成功消息。
-
用户登录功能
- 用例描述:用户可以通过提供邮箱和密码进行登录。
- 前置条件:用户已注册。
- 测试步骤:
- 发送POST请求到
/api/auth/login
,提供有效的邮箱和密码。 - 验证响应状态码为200。
- 验证响应消息为"Login successful"。
- 验证响应头中包含JWT token。
- 发送POST请求到
- 预期结果:用户登录成功,返回200状态码和成功消息,并包含JWT token。
-
获取用户信息
- 用例描述:用户可以通过提供有效的JWT token获取自己的信息。
- 前置条件:用户已登录并获取到JWT token。
- 测试步骤:
- 发送GET请求到
/api/user/profile
,在请求头中提供JWT token。 - 验证响应状态码为200。
- 验证响应中包含用户信息(不包括密码)。
- 发送GET请求到
- 预期结果:成功获取用户信息,返回200状态码和用户信息。
-
更新用户信息
- 用例描述:用户可以更新自己的用户名和邮箱。
- 前置条件:用户已登录并获取到JWT token。
- 测试步骤:
- 发送PUT请求到
/api/user/profile
,在请求头中提供JWT token,并在请求体中提供新的用户名和邮箱。 - 验证响应状态码为200。
- 验证响应消息为"User updated successfully"。
- 验证响应中包含更新后的用户信息。
- 发送PUT请求到
- 预期结果:成功更新用户信息,返回200状态码和成功消息,并包含更新后的用户信息。
-
修改密码
- 用例描述:用户可以通过提供当前密码和新密码修改自己的密码。
- 前置条件:用户已登录并获取到JWT token。
- 测试步骤:
- 发送PUT请求到
/api/user/reset/password
,在请求头中提供JWT token,并在请求体中提供当前密码和新密码。 - 验证响应状态码为200。
- 验证响应消息为"Password changed successfully"。
- 发送PUT请求到
- 预期结果:成功修改用户密码,返回200状态码和成功消息。
-
用户注销
- 用例描述:用户可以注销登录。
- 前置条件:用户已登录并获取到JWT token。
- 测试步骤:
- 发送POST请求到
/api/user/logout
,在请求头中提供JWT token。 - 验证响应状态码为200。
- 验证响应消息为"User logged out successfully"。
- 发送POST请求到
- 预期结果:成功注销用户,返回200状态码和成功消息。
-
管理员升级普通用户为管理员
- 用例描述:超级管理员可以将普通用户升级为管理员。
- 前置条件:超级管理员已登录并获取到JWT token。
- 测试步骤:
- 发送PUT请求到
/api/user/upgrade2admin
,在请求头中提供超级管理员的JWT token,并在请求体中提供要升级的用户ID。 - 验证响应状态码为200。
- 验证响应消息为"User upgraded to admin successfully"。
- 发送PUT请求到
- 预期结果:成功将用户升级为管理员,返回200状态码和成功消息。
边界用例
-
用户注册 - 重复注册
- 用例描述:用户不能使用已存在的用户名或邮箱进行注册。
- 前置条件:用户已注册。
- 测试步骤:
- 发送POST请求到
/api/auth/register
,提供已存在的用户名或邮箱。 - 验证响应状态码为400。
- 验证响应消息为"User already exists"。
- 发送POST请求到
- 预期结果:注册失败,返回400状态码和错误消息。
-
用户登录 - 错误密码
- 用例描述:用户不能使用错误的密码登录。
- 前置条件:用户已注册。
- 测试步骤:
- 发送POST请求到
/api/auth/login
,提供有效的邮箱和错误的密码。 - 验证响应状态码为400。
- 验证响应消息为"Invalid credentials"。
- 发送POST请求到
- 预期结果:登录失败,返回400状态码和错误消息。
-
获取用户信息 - 无效JWT token
- 用例描述:用户不能使用无效的JWT token获取用户信息。
- 前置条件:无。
- 测试步骤:
- 发送GET请求到
/api/user/profile
,在请求头中提供无效的JWT token。 - 验证响应状态码为401。
- 验证响应消息为"Token is not valid"。
- 发送GET请求到
- 预期结果:获取用户信息失败,返回401状态码和错误消息。
-
更新用户信息 - 无效JWT token
- 用例描述:用户不能使用无效的JWT token更新用户信息。
- 前置条件:无。
- 测试步骤:
- 发送PUT请求到
/api/user/profile
,在请求头中提供无效的JWT token,并在请求体中提供新的用户名和邮箱。 - 验证响应状态码为401。
- 验证响应消息为"Token is not valid"。
- 发送PUT请求到
- 预期结果:更新用户信息失败,返回401状态码和错误消息。
-
修改密码 - 当前密码错误
- 用例描述:用户不能使用错误的当前密码修改密码。
- 前置条件:用户已注册并登录。
- 测试步骤:
- 发送PUT请求到
/api/user/reset/password
,在请求头中提供JWT token,并在请求体中提供错误的当前密码和新密码。 - 验证响应状态码为400。
- 验证响应消息为"Current password is incorrect"。
- 发送PUT请求到
- 预期结果:修改密码失败,返回400状态码和错误消息。
-
用户注销 - 无效JWT token
- 用例描述:用户不能使用无效的JWT token注销登录。
- 前置条件:无。
- 测试步骤:
- 发送POST请求到
/api/user/logout
,在请求头中提供无效的JWT token。 - 验证响应状态码为401。
- 验证响应消息为"Token is not valid"。
- 发送POST请求到
- 预期结果:注销登录失败,返回401状态码和错误消息。
-
管理员升级普通用户为管理员 - 非超级管理员操作
- 用例描述:非超级管理员不能将普通用户升级为管理员。
- 前置条件:普通管理员或普通用户已登录并获取到JWT token。
- 测试步骤:
- 发送PUT请求到
/api/user/upgrade2admin
,在请求头中提供普通管理员或普通用户的JWT token,并在请求体中提供要升级的用户ID。 - 验证响应状态码为403。
- 验证响应消息为"Access denied"。
- 发送PUT请求到
- 预期结果:升级用户为管理员失败,返回403状态码和错误消息。
这些测试用例涵盖了用户管理模块中注册、登录、获取用户信息、更新用户信息、修改密码、注销登录、以及管理员升级用户的正向功能和边界情景。通过执行这些测试,可以确保用户管理模块的功能和安全性。
测试结果
npm test
> ore@0.0.1 test
> ts-node-dev tests/**.test.ts
[INFO] 13:14:50 ts-node-dev ver. 2.0.0 (using ts-node ver. 10.9.2, typescript ver. 5.5.3)
---------------------测试开始---------------------
测试 1: √ 成功 √
期望: {
url: '/auth/register',
method: 'post',
data: {
username: 'user',
email: 'user@example.com',
password: 'password123'
},
expectedStatusCode: 201,
expectedData: { message: 'User registered successfully' }
}
------------------------------
测试 2: √ 成功 √
期望: {
url: '/auth/login',
method: 'post',
data: { email: 'user@example.com', password: 'password123' },
expectedStatusCode: 200,
expectedData: { message: 'Login successful' }
}
------------------------------
测试 3: √ 成功 √
期望: {
url: '/auth/register',
method: 'post',
data: {
username: 'user',
email: 'user@example.com',
password: 'password123'
},
expectedStatusCode: 400,
expectedData: { message: 'User already exists' }
}
------------------------------
测试 4: × 错误 ×
期望: {
url: '/user/profile',
method: 'get',
headersFunc: [AsyncFunction: headersFunc],
expectedStatusCode: 200,
expectedData: { username: 'user', email: 'user@example.com', role: 'user' }
}
预期状态码: 200, 实际状态码: 200
预期数据: { username: 'user', email: 'user@example.com', role: 'user' }
实际数据: {
_id: '668f57f4e1d9729fc7847182',
username: 'user',
email: 'user@example.com',
role: 'user',
__v: 0
}
详细差异: [
{
"_id": "668f57f4e1d9729fc7847182",
"username": "user",
"email": "user@example.com",
"role": "user",
"__v": 0
}
]
------------------------------
测试 5: × 错误 ×
期望: {
url: '/user/profile',
method: 'put',
data: { username: 'newuser', email: 'newuser@example.com' },
headersFunc: [AsyncFunction: headersFunc],
expectedStatusCode: 200,
expectedData: {
message: 'User updated successfully',
user: { username: 'newuser', email: 'newuser@example.com', role: 'user' }
}
}
预期状态码: 200, 实际状态码: 200
预期数据: {
message: 'User updated successfully',
user: { username: 'newuser', email: 'newuser@example.com', role: 'user' }
}
实际数据: {
message: 'User updated successfully',
user: {
_id: '668f57f4e1d9729fc7847182',
username: 'newuser',
email: 'newuser@example.com',
password: '$2a$10$XxU/Gm.lEv700Rt9t5yCguUVx4BWZpBDPTKLqe2OpDq9jcoBZsdcu',
role: 'user',
__v: 0
}
}
详细差异: [
{
"message": "User updated successfully",
"user": {
"_id": "668f57f4e1d9729fc7847182",
"username": "newuser",
"email": "newuser@example.com",
"password": "$2a$10$XxU/Gm.lEv700Rt9t5yCguUVx4BWZpBDPTKLqe2OpDq9jcoBZsdcu",
"role": "user",
"__v": 0
}
}
]
------------------------------
测试 6: √ 成功 √
期望: {
url: '/user/reset/password',
method: 'put',
data: { currentPassword: 'password123', newPassword: 'newpassword123' },
headersFunc: [AsyncFunction: headersFunc],
expectedStatusCode: 200,
expectedData: { message: 'Password changed successfully' }
}
------------------------------
测试 7: √ 成功 √
期望: {
url: '/user/logout',
method: 'post',
headersFunc: [AsyncFunction: headersFunc],
expectedStatusCode: 200,
expectedData: { message: 'User logged out successfully' }
}
------------------------------
测试 8: × 错误 ×
期望: {
url: '/user/upgrade2admin',
method: 'put',
data: { userId: 'xxx' },
headersFunc: [AsyncFunction: headersFunc],
expectedStatusCode: 200,
expectedData: { message: 'User upgraded to admin successfully' }
}
预期状态码: 200, 实际状态码: 500
预期数据: { message: 'User upgraded to admin successfully' }
实际数据: { error: 'Server error' }
详细差异: [
{
"error": "Server error"
}
]
------------------------------
测试 9: √ 成功 √
期望: {
url: '/user/profile',
method: 'get',
headers: { Authorization: 'Bearer invalid_token' },
expectedStatusCode: 401,
expectedData: { error: 'Token is not valid' }
}
------------------------------
测试 10: √ 成功 √
期望: {
url: '/user/profile',
method: 'put',
data: { username: 'anotheruser', email: 'anotheruser@example.com' },
headers: { Authorization: 'Bearer invalid_token' },
expectedStatusCode: 401,
expectedData: { error: 'Token is not valid' }
}
------------------------------
测试 11: √ 成功 √
期望: {
url: '/user/reset/password',
method: 'put',
data: { currentPassword: 'wrongpassword', newPassword: 'newpassword123' },
headersFunc: [AsyncFunction: headersFunc],
expectedStatusCode: 400,
expectedData: { error: 'Current password is incorrect' }
}
------------------------------
测试 12: √ 成功 √
期望: {
url: '/user/logout',
method: 'post',
headers: { Authorization: 'Bearer invalid_token' },
expectedStatusCode: 401,
expectedData: { error: 'Token is not valid' }
}
------------------------------
测试 13: √ 成功 √
期望: {
url: '/user/upgrade2admin',
method: 'put',
data: { userId: 'user_id_to_upgrade' },
headersFunc: [AsyncFunction: headersFunc],
expectedStatusCode: 403,
expectedData: { error: 'Access denied' }
}
------------------------------
---------------------测试结束---------------------
问题总结与解决方案
在接口测试结果中,我们发现了三个问题,升级为管理员的这个是因为用户id填写错误先略过。目前,我们将重点解决错误4和错误5。接口处理逻辑是正确的,但输出结果不符合预期。因此,我们需要统一处理输出字段。
新增辅助函数
在 src/utils/dict.ts
文件中新增一个辅助函数 extractKeysFromObject
,该函数用于从输入对象中提取指定键的键值对,并返回一个新的对象。
// src/utils/dict.ts
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* 从输入对象中提取指定键的键值对,返回新的对象。
* @param inputObject - 输入对象
* @param keysToExtract - 要提取的键的数组
* @returns 包含提取键值对的新对象
*/
export function extractKeysFromObject(
inputObject: Record<string, any>,
keysToExtract: string[]
): Record<string, any> {
// 创建一个空对象来存储提取的键值对
const extractedObject: Record<string, any> = {};
// 遍历 keysToExtract 数组
keysToExtract.forEach((key) => {
// 检查输入对象是否包含指定的键
if (Object.prototype.hasOwnProperty.call(inputObject, key)) {
// 如果包含,将键值对添加到提取的对象中
extractedObject[key] = inputObject[key];
}
});
// 返回包含提取键值对的新对象
return extractedObject;
}
更新控制器函数
在 src/controllers/userController.ts
文件中,我们使用 extractKeysFromObject
函数来处理返回的字段。
import { Request, Response } from "express";
import UserModel from "../models/UserModel";
import { extractKeysFromObject } from "../utils/dict";
const userFields = ["username", "email", "role"];
export const getUserProfile = async (req: Request, res: Response) => {
try {
const user = await UserModel.findById(req.user?.userId).select("-password");
if (!user) {
return res.status(404).json({ error: "User not found" });
}
const filteredUser = extractKeysFromObject(user.toObject(), userFields);
res.status(200).json(filteredUser);
} catch (error) {
res.status(500).json({ error: "Server error" });
}
};
export const updateUserProfile = async (req: Request, res: Response) => {
const { username, email } = req.body;
try {
const user = await UserModel.findById(req.user?.userId);
if (!user) {
return res.status(404).json({ error: "User not found" });
}
user.username = username || user.username;
user.email = email || user.email;
await user.save();
const filteredUser = extractKeysFromObject(user.toObject(), userFields);
res.status(200).json({ message: "User updated successfully", user: filteredUser });
} catch (error) {
res.status(500).json({ error: "Server error" });
}
};
export const changePassword = async (req: Request, res: Response) => {
const { currentPassword, newPassword } = req.body;
try {
const user = await UserModel.findById(req.user?.userId);
if (!user) {
return res.status(404).json({ error: "User not found" });
}
const isMatch = await user.comparePassword(currentPassword);
if (!isMatch) {
return res.status(400).json({ error: "Current password is incorrect" });
}
user.password = newPassword;
await user.save();
res.status(200).json({ message: "Password changed successfully" });
} catch (error) {
res.status(500).json({ error: "Server error" });
}
};
export const logout = async (req: Request, res: Response) => {
// 清除客户端保存的JWT Token
res.status(200).json({ message: "User logged out successfully" });
};
export const upgradeToAdmin = async (req: Request, res: Response) => {
const { userId } = req.body;
try {
const user = await UserModel.findById(userId);
if (!user) {
return res.status(404).json({ error: "User not found" });
}
user.role = "admin";
await user.save();
const filteredUser = extractKeysFromObject(user.toObject(), userFields);
res.status(200).json({ message: "User upgraded to admin successfully", user: filteredUser });
} catch (error) {
res.status(500).json({ error: "Server error" });
}
};
解释
- 引入
extractKeysFromObject
函数:我们在每个需要过滤返回字段的地方都使用这个函数。 - 定义
userFields
:我们定义一个包含需要返回字段的数组userFields
,并在每个控制器函数中使用它。 - 调用
extractKeysFromObject
:在获得用户数据后,我们将其转换为普通对象 (user.toObject()
),然后调用extractKeysFromObject
函数来提取指定字段。
通过这种方式,我们可以灵活地控制返回的数据字段,确保返回的数据结构与预期一致。
总结
本文详细探讨了如何在一个基于Node.js和TypeScript的Web应用中实现用户注册与登录、JWT认证与授权、RBAC权限管理、用户信息管理、密码管理、用户退出、管理员脚本创建以及管理员升级普通用户为管理员的接口等功能
在后续第4章中,我们将通过实际代码示例,展示如何从零开始实现文件上传和下载功能。我们将介绍如何配置和使用multer
处理文件上传,并实现支持分片上传和断点续传的高级功能。此外,我们还将编写高效的文件下载接口,优化文件读取和传输过程,确保用户体验的流畅和系统的稳定。通过本章的学习,读者将能够掌握文件处理的核心技术,应用于各种实际项目中。
转载自:https://juejin.cn/post/7389955238058672155