一、介绍项目:

是一个简易的记账本,用户可以实现注册、鉴权登录,添加、查看、修改、删除记账单。

使用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、核心原理

  1. 用户访问 URL → 进入路由匹配
  2. 经过登录校验中间件 → 判断是否已登录
  3. 交给对应逻辑处理 → 返回页面或数据
  4. 统一格式、统一权限、统一错误处理

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、模块重点

  1. web /api 分离:页面和接口分开,便于维护。
  2. 统一登录校验:所有需要权限的路由都使用 checkLogin 中间件。
  3. 用户数据隔离:所有账单操作都根据 userID 筛选,保证安全。
  4. 统一响应格式:API 接口统一返回 { code, msg, data },前端更容易处理。

4.3、登陆校验:

这里有两种方式实现登陆校验:

session和token的区别
验证方式 凭证存储位置 校验逻辑 适用场景
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 是整个项目最核心、最抽象的知识点:

  • 容易混淆 凭证存在哪里、谁来保管、如何校验
  • 分不清 sidsecret、签名、加密的区别
  • 不理解为什么 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 绑定,实现数据隔离

五、项目收获总结

通过本项目,掌握了:

  1. Express 项目从 0 到 1 的搭建流程
  2. 路由拆分、中间件使用、错误处理
  3. MongoDB 数据库连接与模型设计
  4. Session 与 JWT 两套登录校验原理与实现
  5. 项目安全机制与企业级开发规范
  6. 从 “会写代码” 到 “懂原理、能上线” 的完整开发思维
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐