简易记账本:Node.js + Express + MongoDB 全栈实战小项
一、介绍项目:
是一个简易的记账本,用户可以实现注册、鉴权登录,添加、查看、修改、删除记账单。
使用nodejs/express、mongodb/mongoose、EJS模块引擎、session+cookie/token、moment、md5等工具。
二、整体思路:
1、页面渲染:用户访问页面时,浏览器向服务器发送请求,服务器通过EJS模块渲染的HTML文档,并返回,最终在浏览器中渲染页面。
2、注册与鉴权:用户进入【注册】页面,提交账号和密码,服务器对密码进行【md5】加密后存入数据库MongoDB。登陆成功后生成身份凭证 (Session+Cookie或JWT Token)返回给浏览器;浏览器把token保存在本地(或由Cookie自动携带Session),后续请求均需携带凭证,实现【鉴权登录】。
3、权限校验:服务器接收请求后,先校验凭证的有效性(Token解析/Session验证),校验失败则返回“请登陆”,校验通过才会进行下一步业务逻辑。
4、账单操作:鉴权通过后,服务器会通过Mongoose对MongoDB数据库中账单数据,进行增删改查操作,操作完成后返回渲染后的结果页面,页面内置跳转页面逻辑,其中账单时间会通过moment进行格式化处理,保证数据统一。
三、搭建项目步骤:
一、基础环境配置:
3.1.1、使用【express-generator】快速搭建项目(使用前需先安装包):
终端输入 :
express --view=ejs 项目名

生成项目后必须 【cd 项目名 + npm install 】安装生成器自带的依赖,否则项目跑不起来
3.1.2、手动安装所需的包:
生成器默认只装了 express、ejs、cookie-parser 等基础包,需要手动安装。
数据库相关的包:mongoose
鉴权相关包:express-session、connect-mongo(session持久化)、jsonwebtoken(jwt)、md5(密码加密)
工具类:moment(时间格式化)
npm install mongoose express-session connect-mongo jsonwebtoken md5 moment
3.1.3、配置数据库连接:
在项目根目录建【config】文件夹,config文件夹下创建config.js文件,写入MongoDB连接配置(IP,端口,数据库名)
在项目根目录建【db】文件夹,导入MongoDB配置文件,写入MongoDB连接数据库逻辑
在app.js中引入并执行数据库连接代码,确保连接数据库
二、业务层面开发
3.2.1、设计数据模型
根目录下创建【models】文件夹,写UserModel,js(用户)、AccountModel(账本)
定义字段:用户表(用户账号、用户密码)、账本(标题、金额、时间、备注)
3.2.2、根据需求改造路由:
在文件夹routes下创建两个文件夹【web】和【api】
web文件夹:存放服务器渲染的路由,返回HTML页面
api文件夹:存放接口请求,返回JSON数据
app.use('/', web路由)
app.use('/api', api路由))
登陆路由内写入session或创建token
3.2.3、编写ejs模板
在view文件夹下新增
鉴权相关:login.ejs、reg.ejs
账本相关:list.ejs、create.ejs、sucess.ejs、error.ejs
三、调整启动脚本
生成器默认是 npm start (node ./bin/www),可以改成nodemon ,实现热更新

四、核心模块:
4.1、数据库模型设计:
4.2、路由模块:
4.2.1、模块作用
路由模块是整个项目的请求入口,负责接收客户端的请求,分配到对应的处理逻辑,最终返回页面或数据。本项目按照请求用途将路由分为两类,实现逻辑分离、结构清晰。
4.2.2、设计思想
- web 路由:负责服务端渲染,返回 HTML 页面(登录页、首页、账单列表页等)。
- api 路由:负责数据接口,返回 JSON 格式数据(登录、增删改查账单)。
这样拆分的好处:
- 页面逻辑和接口逻辑互不干扰
- 便于统一做登录校验
- 符合企业级项目开发规范
4.2.3、目录结构
routes/
├── web/ # 页面路由(服务端渲染)
├── api/ # 接口路由(返回JSON)
└── app.js 中统一挂载
4.2.4、核心原理
- 用户访问 URL → 进入路由匹配
- 经过登录校验中间件 → 判断是否已登录
- 交给对应逻辑处理 → 返回页面或数据
- 统一格式、统一权限、统一错误处理
4.2.5、实现方式
1、路由文件示例(web 页面路由)
const express = require('express');
const router = express.Router();
const checkLogin = require('../middlewares/checkLogin');
// 首页路由
router.get('/', checkLogin, (req, res) => {
res.render('index', { username: req.session.username });
});
// 账单列表页面
router.get('/account', checkLogin, (req, res) => {
res.render('account');
});
module.exports = router;
2、接口路由示例(api 数据路由)
const express = require('express');
const router = express.Router();
const checkLogin = require('../middlewares/checkLogin');
const AccountModel = require('../models/AccountModel');
// 获取账单列表接口
router.get('/account', checkLogin, async (req, res) => {
const data = await AccountModel.find({ userID: req.session.userID });
res.json({ code: '0000', msg: '获取成功', data });
});
module.exports = router;
3、在 app.js 中挂载路由
// 页面路由
app.use('/', require('./routes/web/index'));
app.use('/', require('./routes/web/account'));
// 接口路由
app.use('/api', require('./routes/api/auth'));
app.use('/api', require('./routes/api/account'));
4.2.6、模块重点
- web /api 分离:页面和接口分开,便于维护。
- 统一登录校验:所有需要权限的路由都使用
checkLogin中间件。 - 用户数据隔离:所有账单操作都根据
userID筛选,保证安全。 - 统一响应格式:API 接口统一返回
{ code, msg, data },前端更容易处理。
4.3、登陆校验:
这里有两种方式实现登陆校验:
| 验证方式 | 凭证存储位置 | 校验逻辑 | 适用场景 |
| Session+Cookie | 服务器存Session,客户端存Cookie(sid) | 客户端携带sid,服务器查Session,校验身份 | 服务端渲染 |
| Token | 客户端存(LocalStorage/Cookie) | 客户端携带Token,服务端验密解码,校验身份 | 前后端分离 |
4.3.1、传统的cookie+session:
原理:
用户登录成功后,服务器不会保存账号 / 密码等敏感信息,仅存储用户的唯一标识(如 userID);随后服务器生成一段原始随机字符串,并使用服务端私有密钥 secret(如本项目配置的 atguigu)对该随机字符串进行 HMAC 签名,将「原始随机字符串 + 签名」拼接成唯一且防篡改的 Session ID(sid);服务器通过 Set-Cookie 响应头,将包含签名的完整 sid 写入客户端 Cookie(客户端无需手动处理,后续所有请求都会自动携带这个 sid);
服务器接收请求时,首先从 Cookie 中提取完整的 sid,按分隔符拆分出「原始随机字符串」和「客户端传来的签名」;接着用相同的 secret 密钥对原始随机字符串重新进行签名,对比 “重新生成的签名” 和 “客户端传来的签名” 是否一致:
- 若签名不一致,说明 sid 被客户端伪造 / 篡改,直接判定身份验证失败;
- 若签名一致,说明 sid 是服务端合法生成的,再通过该 sid 查询数据库内的 Session 数据(匹配对应的用户唯一标识),最终完成用户身份验证。
实现方式:
1、配置Session中间件(app.js)
// 导入express - session
const session = require('express-session')
const MongoStore = require('connect-mongo').default
// 导入配置文件
const { DBHOST, DBPORT, DBNAME } = require('./config/config')
// 配置Session(核心:将Session持久化到MongoDB,避免服务重启丢失)
app.use(session({
name: 'sid',// Cookie名称(前端存的键名)
secret: 'atguigu', // 核心:sid的签名密钥,防篡改
saveUninitialized: false,// 不保存未初始化的Session(优化性能)
resave: false, // 不重复保存未修改的Session(优化性能)
store: new MongoStore({ // Session持久化到MongoDB
mongoUrl: `mongodb://${DBHOST}:${DBPORT}/${DBNAME}`
}),
cookie: {
httpOnly: true, // 禁止前端JS操作Cookie(防XSS攻击)
maxAge: 1000 * 60 * 60 * 24 // 有效期1天
}
}))
2、登陆成功后生成sid
const express = require('express');
const router = express.Router();
const UserModel = require('../../models/UserModel');
const md5 = require('md5'); // 密码加密
// 登录接口
router.post('/login', async (req, res) => {
try {
const { username, password } = req.body;
// 1. 查询用户(密码需和数据库中md5加密后的对比)
const user = await UserModel.findOne({
username,
password: md5(password)
});
if (!user) {
return res.json({ code: '2001', msg: '账号或密码错误' });
}
// 2. 登录成功:生成Session(核心!服务端记录用户身份)
req.session.userID = user._id; // 存用户ID
req.session.username = user.username; // 存用户名
// 3. 返回成功响应(Cookie由Session中间件自动写入客户端)
res.json({
code: '0000',
msg: '登录成功',
data: { username: user.username }
});
} catch (err) {
res.json({ code: '2002', msg: '登录失败', data: null });
}
});
3、编写登陆校验中间件:
// 核心:拦截未登录的请求
module.exports = (req, res, next) => {
// 1. 检查Session中是否有用户信息(已登录)
if (req.session.userID) {
next(); // 放行,执行后续逻辑
} else {
// 2. 未登录:区分场景返回不同结果
if (req.originalUrl.startsWith('/api')) {
// API请求:返回JSON错误
res.json({ code: '2003', msg: '请先登录' });
} else {
// 页面请求:跳转到登录页
res.redirect('/login');
}
}
};
4、使用中间件保护路由
// 保护账单列表页(未登录跳登录页)
router.get('/account', checkLogin, async (req, res) => { ... });
// 保护新增账单接口(未登录返回JSON错误)
router.post('/api/account', checkLogin, async (req, res) => { ... });
5、退出登录
router.get('/logout', (req, res) => {
// 销毁Session(服务端)+ 清除Cookie(客户端)
req.session.destroy((err) => {
if (err) {
return res.json({ code: '2004', msg: '退出失败' });
}
// 跳转到登录页
res.redirect('/login');
});
});
4.3.2、JWT Token:
原理:
用户登录成功后,服务器不会加密账号 / 密码等敏感信息,仅提取用户的唯一标识(如 userID、username)构建成 Payload(载荷),并搭配指定的加密算法(如 HS256)构建 Header(头部);随后服务器使用私有密钥(如 atguigu)对 Header 和 Payload 进行签名,将「Header + Payload + 签名」三部分通过 Base64 编码并拼接,生成唯一且防篡改的 JWT Token(注:Payload 为 Base64 编码而非加密,可解码但不可篡改);服务器将 Token 直接返回给客户端,客户端需手动将 Token 存储在 LocalStorage/Cookie 中(不会自动携带)。
客户端后续发起请求时,需手动在请求头(如 token 字段)中携带该 Token;服务器接收请求后,首先拆分 Token 得到 Header、Payload 和签名三部分,提取 Header 中的加密算法,并用相同的私有密钥对 Header 和 Payload 重新生成签名;对比 “重新生成的签名” 与 “Token 中的签名” 是否一致:
- 若签名不一致,说明 Token 被伪造 / 篡改,直接判定身份验证失败;
- 若签名一致,说明 Token 是服务端合法生成的,服务器解码 Payload 提取用户唯一标识,无需查询数据库即可完成用户身份验证(无状态鉴权)。
实现方式:
1、登陆成功生成Token
const jwt = require('jsonwebtoken');
// JWT登录接口
router.post('/login/token', async (req, res) => {
try {
const { username, password } = req.body;
const user = await UserModel.findOne({
username,
password: md5(password)
});
if (!user) {
return res.json({ code: '2001', msg: '账号或密码错误' });
}
// 生成JWT Token(核心:用密钥签名,包含用户信息)
const token = jwt.sign(
{ userID: user._id, username: user.username }, // Payload(不存敏感信息)
'atguigu', // 签名密钥(服务端私有,绝对不能泄露)
{ expiresIn: '1d' } // 有效期1天
);
res.json({
code: '0000',
msg: '登录成功',
data: { token } // 返回Token给客户端
});
} catch (err) {
res.json({ code: '2002', msg: '登录失败' });
}
});
2、JWT校验中间件
const jwt = require('jsonwebtoken');
module.exports = (req, res, next) => {
// 1. 获取Token(从请求头的token字段获取)
const token = req.get('token');
if (!token) {
return res.json({ code: '2003', msg: '请先登录' });
}
// 2. 验证Token(解密+验签)
jwt.verify(token, 'atguigu', (err, decoded) => {
if (err) {
// Token无效(过期/篡改/密钥错误)
return res.json({ code: '2001', msg: 'Token无效' });
}
// 3. 验证通过:将用户信息挂载到req上,供后续使用
req.user = decoded;
next();
});
};
3、使用中间件保护路由
// 用JWT校验保护账单接口
router.post('/api/account', checkToken, async (req, res) => {
// 从req.user获取用户信息(无需查Session)
console.log('当前登录用户:', req.user.username);
// 后续业务逻辑...
});
第五章 项目重难点分析
本项目在开发过程中,难点主要集中在身份认证、安全校验、项目结构规范三个方面。这些内容既是开发中的容易出错点,也是理解后端逻辑的关键。
一、登录校验原理理解难度高
Session 和 JWT 是整个项目最核心、最抽象的知识点:
- 容易混淆 凭证存在哪里、谁来保管、如何校验
- 分不清
sid、secret、签名、加密的区别 - 不理解为什么 Cookie 会自动携带,而 Token 需要手动携带
- 不知道 Session 为什么要持久化、JWT 为什么不能存敏感信息
这部分是新手最容易卡壳的地方,也是面试必问内容,必须理解原理而不是只抄代码。
二、Session 安全机制复杂
secret作用是签名防篡改,不是加密用户数据- Cookie 的
httpOnly防 XSS,sameSite防 CSRF - 服务端重启后登录状态不丢失,必须做 Session 持久化到数据库
- 未登录拦截需要区分页面路由和 API 接口,返回不同格式
任何一个配置错误,都会导致登录失效、权限失效、安全漏洞。
三、JWT 验签逻辑容易误解
- JWT 的 Payload 是 Base64 编码,不是加密
- 验证核心是 验签,不是解密账号密码
- JWT 是无状态的,服务端不存储,无法主动作废
- Token 必须前端手动放在请求头里,不会自动携带
理解这些,才算真正掌握前后端分离的鉴权机制。
四、路由结构拆分与权限控制
- 按
web/api拆分路由,是企业级项目规范 - 所有需要登录的页面 / 接口必须统一使用中间件保护
- 权限控制逻辑一旦混乱,会出现未登录可访问、越权访问等问题
五、数据库与业务逻辑耦合
- 用户密码必须加密存储,不能明文
- 账单数据必须和用户 ID 关联,实现数据隔离
- 错误处理不规范会导致服务崩溃或前端无法正常提示
第六章 核心知识总结
一、项目整体架构总结
本项目是基于 Node.js + Express + MongoDB 开发的记账本系统,采用经典的 MVC 结构:
- Model:数据模型(用户、账单)
- View:页面渲染(EJS 模板)
- Controller:路由与业务逻辑
- 中间件:登录校验、Session、解析请求等
结构清晰、职责分离,便于维护和扩展。
二、路由模块核心总结
项目将路由分为两类:
- web 路由:服务端渲染,返回 HTML 页面
- api 路由:提供接口,返回 JSON 数据
这种拆分让页面逻辑和接口逻辑分离,是后端项目最常用的规范结构。
三、登录鉴权核心总结
项目实现了两套登录校验方案:
1)Cookie + Session(服务端渲染推荐)
- 服务端存储用户信息
- 客户端 Cookie 存储 sid
- sid 通过 secret 签名防篡改
- 浏览器自动携带 Cookie
- 可实现主动销毁登录状态
- 需配置持久化、防 XSS、防 CSRF
2)JWT Token(前后端分离推荐)
- 服务端不存储状态
- 客户端存储 Token,手动携带
- 核心是验签,确保不被篡改
- 无状态、易扩展、跨域友好
- 不能存敏感信息,无法主动作废
四、安全知识总结
- 密码使用 md5 加密存储
- Session 使用 secret 签名防篡改
- Cookie 设置
httpOnly防止 XSS - Cookie 设置
sameSite防止 CSRF - 接口统一登录校验,防止越权访问
- 账单与用户 ID 绑定,实现数据隔离
五、项目收获总结
通过本项目,掌握了:
- Express 项目从 0 到 1 的搭建流程
- 路由拆分、中间件使用、错误处理
- MongoDB 数据库连接与模型设计
- Session 与 JWT 两套登录校验原理与实现
- 项目安全机制与企业级开发规范
- 从 “会写代码” 到 “懂原理、能上线” 的完整开发思维
更多推荐

所有评论(0)