从前端模拟到全栈认证:我的论坛 JWT 实战复盘

0 阅读5分钟

前言

我的校园论坛项目从 Vue3 + localStorage 模拟数据起步,到接入 MongoDB 实现数据持久化,再到今天完成 JWT 认证和权限控制——整个过程中踩过的坑、总结的模式,促使我写下这篇文章。

这是一份实战复盘。目标是梳理“JWT 认证到底做了什么”、“为什么要这样设计”、“中间踩了哪些坑”。如果你也在做自己的全栈项目,希望这篇文章能帮你少走一些弯路。


一、项目背景

技术栈

  • 前端:Vue3 (Composition API) + Pinia + Vue Router + Vite
  • 后端:Node.js + Express(路由模块化)
  • 数据库:MongoDB(本地部署 + Mongoose ODM)
  • 认证:bcrypt 密码加密 + JWT 身份令牌

JWT 认证完成前的项目状态

  • 帖子 CRUD、评论(嵌套子文档)、用户注册登录均已从内存数组迁移至 MongoDB
  • 密码使用 bcrypt 加密存储,登录时用 bcrypt.compare 验证
  • 任何人打开页面就能发帖、点赞、删别人的帖子——没有身份认证,也没有权限控制

JWT 认证的目标

  1. 登录成功后,后端签发一个加密的 token(包含用户身份信息)
  2. 前端把这个 token 存起来,以后每次操作都带过去
  3. 后端收到请求时,先解析 token 确认“你是谁”,再决定“你能不能做这件事”
  4. 不同角色拥有不同权限:只有作者能删自己的帖子,只有评论者能改自己的评论,帖主可以删评论

二、JWT 认证核心流程

第一步:登录时签发 token

后端 routes/users.js 登录接口

const jwt = require('jsonwebtoken');

// 登录成功后
const token = jwt.sign(
  { sno: user.sno, _id: user._id },  // 把用户信息编码进 token
  process.env.JWT_SECRET,             // 用密钥加密
  { expiresIn: '7d' }                 // 7 天后过期
);
res.json({ user: safeUser, token });

jwt.sign 做了三件事:接收要编码的数据、用密钥加密、设置过期时间。生成的 token 是一串由三部分组成的乱码(Header.Payload.Signature),可以通过 JSON.parse(atob(token.split('.')[1])) 解析出 Payload 看到里面的用户信息。

前端存储 token

// user store 的 userLogin action 中
const data = await res.json()
currentUser.value = data.user
localStorage.setItem('forum-token', data.token)

退出登录时清除

function logout() {
  currentUser.value = null;
  localStorage.removeItem('forum-token');
}

第二步:创建 auth 中间件解析 token

新建 middleware/auth.js

const jwt = require('jsonwebtoken');

module.exports = (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) {
    return res.status(401).json({ error: '未登录,请先登录' });
  }
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;  // 把用户信息挂到 req.user
    next();
  } catch (err) {
    res.status(401).json({ error: 'Token 无效或已过期,请重新登录' });
  }
};

关键理解

  • 前端发来的 token 在 req.headers.authorization 里,格式是 Bearer <token>,所以需要 .split(' ')[1] 取出
  • jwt.verify 用同样的密钥解开 token,如果被篡改或过期会抛异常
  • next() 是中间件放行的信号,必须调用,否则请求会卡住

第三步:保护路由

发帖接口加 auth 保护前后对比

// 改造前:任何人可以发帖
postRouter.post('/', async (req, res) => {
  const authorId = '69edd547...' // 硬编码,不知道是谁
})

// 改造后:必须登录,自动知道是谁
postRouter.post('/', auth, async (req, res) => {
  // ...
  author: req.user._id  // 从 token 里解析出来的用户 _id
})

auth 中间件会在进入路由之前先执行,检查 token、解析用户信息、挂到 req.user 上。如果没登录或 token 过期,直接返回 401,不会进入路由。

前端发帖请求加 token

headers: {
  'Content-Type': 'application/json',
  'Authorization': `Bearer ${localStorage.getItem('forum-token')}`
}

第四步:权限控制

只有帖子作者能删/改自己的帖子

const post = await Post.findById(id)
if (post.author.toString() !== req.user._id) {
  return res.status(403).json({ error: '只能编辑自己的帖子' })
}

点赞防重复:在 Post Schema 中新增 likedBy 数组记录点赞用户:

likedBy: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }]

点赞时检查:

if (post.likedBy.includes(req.user._id)) {
  return res.status(400).json({ error: '你已经点过赞了' })
}

评论权限双重检查:删除评论时,评论作者或帖主都能删:

if (user !== comment.author.toString() && user !== post.author.toString()) {
  return res.status(403).json({ error: '无权限删除此评论' })
}

三、踩坑实录

坑一:post.author.toString() 为什么要加 .toString()

打印 typeof post.author 得到 "object",而 typeof req.user._id 得到 "string"。Mongoose 的 ObjectId 在代码里是对象,jwt.sign 转出来的 _id 是字符串——不同类型之间 === 永远为 false。必须用 .toString() 把 ObjectId 转成字符串才能比对。

坑二:编辑评论时用 findByIdAndUpdate 操作子文档

子文档不是独立集合,没有 findByIdAndUpdate 方法。正确做法是先找到父文档 post,再用 post.comments.id(commentId) 拿到子文档引用,直接改属性,最后 await post.save()

坑三:前端评论请求忘记加 token

帖子模块所有请求(发帖、删除、编辑、点赞)都加好了 Authorization 头,但评论的三个请求还是旧版本。导致后端返回 401,前端只显示“删除失败”。前端新增、删除、编辑评论必须和帖子一样带 token。


四、犯错之后

我把这次 JWT 实现的完整流程梳理了一遍,并总结出来一个 接口保护模板

router.METHOD('/path', auth, async (req, res) => {
  // 1. 查资源
  // 2. 判存在
  // 3. 比对所有权(toString() !)
  // 4. 执行操作
  // 5. 返回结果
})

所有需要保护的路由都是这个模板的变种,唯一不同的是“比对所有权”那一行——帖子比 post.author,评论比 comment.author,点赞查 post.likedBy.includes

另一个收获是“先验证核心链路,再补全其他接口”的调试策略。 先把发帖接口保护起来,确认登录、token 签发、中间件解析、req.user._id 写入整条链路跑通后,再按同样的模板补完删除、编辑、点赞、评论。


五、总结

完成 JWT 认证后,我的论坛现在拥有:

  • 用户注册/登录,密码 bcrypt 加密
  • JWT 身份令牌,登录签发,退出清除
  • 所有写操作必须登录
  • 只有作者能删/改自己的帖子
  • 只有评论作者能编辑评论,帖主和评论作者能删评论
  • 防重复点赞

终于从Demo变成一个有身份认证、有权限控制、有数据持久化的全栈产品。

下一步计划:UI 优化、部署上线。


这是我的掘金技术笔记,记录我在校园论坛项目中从“能用就行”到“理解为什么”的成长过程。