Node.js实现仅商品创建者可更新/删除商品的权限控制
商品操作权限控制实现方案
核心问题排查
你现有代码存在3个直接导致权限逻辑无法实现的问题:
- 商品模型的
seller字段设置了unique: true约束,且类型为普通字符串,会导致一个用户只能创建1件商品,也无法和用户ID做关联匹配 - 没有JWT鉴权逻辑,接口无法识别当前发起请求的登录用户身份
- 注册接口生成JWT时错误引用了模型对象的ID,没有存入新注册用户的真实ID,所有注册返回的token都是无效身份
- 创建商品时没有绑定创建者ID,更新、删除操作前没有做身份比对
分步实现代码
1. 修正商品模型定义
把seller字段改为关联用户表的ObjectId类型,移除错误的unique约束:
const mongoose = require('mongoose'); const productSchema = new mongoose.Schema({ name:{ type : String, required : true }, category:{ type : String, required : true }, description :{ type : String, required : true }, price: { type : Number, required : true }, // 修正seller字段:存储商品创建者的用户ID,关联用户模型 seller: { type : mongoose.Schema.Types.ObjectId, ref: 'Register', // 保持和你用户模型导出的名称一致 required : true }, image: { type : String, required : true }, Date:{ type: Date, default: Date.now } }) module.exports = mongoose.model('Product', productSchema);
2. 编写JWT鉴权中间件
新建鉴权中间件文件,所有需要登录的接口先经过该中间件解析当前用户身份:
const jwt = require('jsonwebtoken'); // 注意和你之前JWT签名用的密钥保持一致,如果你要读环境变量就去掉引号,直接写process.env.ACCESS_TOKEN const JWT_SECRET = "process.env.ACCESS_TOKEN"; const authMiddleware = (req, res, next) => { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({error: "未登录,无访问权限"}); } const token = authHeader.split(' ')[1]; try { const decoded = jwt.verify(token, JWT_SECRET); // 将解析出的当前登录用户ID挂载到req对象,后续路由可直接读取 req.userId = decoded._id; next(); } catch (error) { return res.status(401).json({error: "token无效或已过期"}); } } module.exports = authMiddleware;
3. 修复注册接口的token生成bug
你原有注册接口生成token时误用了模型对象的ID,替换为新创建用户的真实ID:
// 找到signup路由里生成accessToken的代码段,替换为以下内容 const accessToken = jwt.sign( { _id: register._id }, // 原来写的Register._id是模型对象,要改成新创建的用户实例register的_id "process.env.ACCESS_TOKEN" );
4. 改造商品路由接口
商品列表接口保持公开可访问,创建、更新、删除接口增加鉴权和权限校验逻辑:
const express = require('express'); const router = express.Router(); const Product = require('./models/Product'); // 替换为你本地的商品模型路径 const authMiddleware = require('./auth'); // 替换为你本地的鉴权中间件路径 // 公开接口:获取所有商品列表,登录/未登录用户都可访问 router.get("/products", async (req, res) => { try { // populate可联表查出商品创建者的用户名、邮箱,不需要可以删掉 const products = await Product.find().populate('seller', 'username email'); return res.status(200).json({ products }); } catch (error) { console.log(error); return res.status(500).json({error: "服务器错误"}); } }); // 需登录接口:创建商品,自动绑定当前登录用户为商品创建者 router.post("/products", authMiddleware, async (req, res) => { try { const { name, category, description, price, image } = req.body; const newProduct = new Product({ name, category, description, price, image, seller: req.userId // 直接取中间件解析出的当前用户ID作为创建者 }); await newProduct.save(); return res.status(201).json({ product: newProduct }); } catch (error) { console.log(error); return res.status(500).json({error: "商品创建失败"}); } }); // 需权限接口:更新商品,仅创建者可操作 router.put("/products/:id", authMiddleware, async (req, res) => { try { const productId = req.params.id; const product = await Product.findById(productId); if (!product) { return res.status(404).json({error: "商品不存在"}); } // 比对商品创建者ID和当前登录用户ID,不一致直接返回403 if (product.seller.toString() !== req.userId) { return res.status(403).json({error: "无权限操作,仅商品创建者可更新"}); } // 权限校验通过,执行更新 const updatedProduct = await Product.findByIdAndUpdate( productId, req.body, { new: true, runValidators: true } ); return res.status(200).json({ product: updatedProduct }); } catch (error) { console.log(error); return res.status(500).json({error: "商品更新失败"}); } }); // 需权限接口:删除商品,仅创建者可操作 router.delete("/products/:id", authMiddleware, async (req, res) => { try { const productId = req.params.id; const product = await Product.findById(productId); if (!product) { return res.status(404).json({error: "商品不存在"}); } // 权限校验 if (product.seller.toString() !== req.userId) { return res.status(403).json({error: "无权限操作,仅商品创建者可删除"}); } // 校验通过执行删除 await Product.findByIdAndDelete(productId); return res.status(200).json({message: "商品删除成功"}); } catch (error) { console.log(error); return res.status(500).json({error: "商品删除失败"}); } }); module.exports = router;
注意事项
- 前端请求创建/更新/删除商品接口时,必须在请求头携带
Authorization: Bearer 登录返回的accessToken,否则会被鉴权中间件拦截 - 因为
seller字段存储的是ObjectId类型,和字符串类型的req.userId比对时必须先转成字符串,否则会因类型不一致一直判定无权限 - 如果你之前已经创建过测试商品数据,因为旧数据的seller字段是字符串类型,需要先清空旧商品数据再测试新逻辑,否则会出现类型匹配错误
内容的提问来源于stack exchange,提问作者Apurv JHA
相关产品推荐
相关产品推荐

