Express+Multer上传权限预校验:如何提前校验分类与用户权限
我明白你的困扰:你想在认证授权中间件里提前校验用户上传的分类权限,但因为Multer的解析时机问题,中间件里拿不到FormData里的category字段,只能等到上传处理函数里才能拿到,导致无法提前拦截非法请求。
问题根源
Multer默认是在路由处理函数中调用upload(req, res)时才会解析FormData。而你的认证中间件是在路由处理函数之前执行的,此时FormData还没被解析,所以req.body里自然没有category这个字段——这就是为什么你在AuthController里只能看到用户认证信息,看不到分类ID的原因。
最佳解决方案:在Multer的fileFilter中做权限校验
fileFilter是Multer提供的钩子函数,它会在文件开始上传前执行,并且此时Multer已经解析了FormData中的文本字段(包括category)。同时,你可以通过认证中间件把用户信息(比如isAdmin)挂载到req.user上(这比存在req.body里安全得多,避免伪造),这样就能在fileFilter里同时拿到用户权限和分类信息,提前拦截非法请求,还能避免无效文件上传。
步骤1:修正认证中间件,将用户信息挂载到req.user
首先,把用户的认证信息(比如isAdmin)从req.body转移到req.user——这是Express认证的标准做法,避免客户端篡改:
// AuthController.ts import { Request, Response, NextFunction } from 'express'; import { Unauthorized } from './your-error-classes'; public static authenticate(req: Request, res: Response, next: NextFunction) { // 这里替换成你的实际认证逻辑(比如JWT解析、会话获取) // 示例:从安全渠道获取用户信息,而非req.body const user = { user_id: 1, isAdmin: true }; req.user = user; // 将用户信息挂载到req.user next(); } public static allowAuthenticated() { return (req: Request, res: Response, next: NextFunction) => { if (!req.user) { return res.status(401).send(new Unauthorized()); } next(); }; }
步骤2:在fileFilter中加入权限校验
修改你的imageFilter,同时校验文件类型和上传权限:
// UploadController.ts import { Request } from 'express'; import multer, { Multer } from 'multer'; import { Unauthorized } from './your-error-classes'; const imageFilter = (req: Request, file: Express.Multer.File, cb: Multer.FileFilterCallback) => { const user = req.user; const category = req.body.category; // 权限校验:普通用户只能上传到users分类 if (!user.isAdmin && category !== 'users') { return cb(new Unauthorized('You are not allowed to upload to this category'), false); } // 文件类型校验 if (!file.originalname.match(/\.(jpg|jpeg|png|gif)$/)) { return cb(new Error('Only image files are allowed!'), false); } cb(null, true); }; // 配置Multer,直接作为中间件使用,而非在处理函数里调用 const upload = multer({ storage: yourDiskStorageConfig, // 你的diskStorage配置 fileFilter: imageFilter, limits: { fileSize: MAX_SIZE } }).single('photo');
步骤3:调整路由顺序,先认证再用Multer中间件
把Multer作为路由中间件放在认证之后,这样fileFilter就能拿到req.user和req.body.category:
// 路由配置 router.post('/upload', AuthController.authenticate, // 先认证,挂载req.user AuthController.allowAuthenticated(), // 确保用户已登录 upload, // Multer中间件,执行fileFilter和文件上传 UploadController.uploadPhoto );
步骤4:简化UploadController的处理函数
因为权限和文件类型已经在fileFilter里校验过了,处理函数只需要生成返回URL即可:
export class UploadController { public static async uploadPhoto(req: Request, res: Response, next: NextFunction) { try { if (!req.file) { return res.status(400).send(new BadRequest('No file uploaded')); } const imageUrl = `${HOST}/static/${req.file.filename}`; res.status(200).json({ imageUrl }); } catch (e) { return res.status(400).send(new BadRequest('Image upload operation is not successful.')); } } }
备选方案:提前解析FormData再校验权限
如果你坚持要在认证中间件里做校验,可以先让Multer解析FormData,再执行权限校验,但这样会先上传文件,若权限不通过需要手动删除文件:
// 路由配置:先解析文件和FormData,再认证校验 router.post('/upload', upload, AuthController.validateUploadCategory, // 自定义权限校验中间件 UploadController.uploadPhoto ); // AuthController.ts新增校验中间件 public static validateUploadCategory(req: Request, res: Response, next: NextFunction) { const user = req.user; const category = req.body.category; if (!user.isAdmin && category !== 'users') { // 删除已上传的文件 fs.unlinkSync(req.file.path); return res.status(403).send(new Unauthorized()); } next(); }
这种方式的缺点是会产生无效的文件上传,需要额外处理文件删除,不如第一种方案优雅。
重要提醒
永远不要把isAdmin这样的权限字段放在req.body里!客户端可以轻易伪造这个字段,导致权限绕过。正确的做法是通过JWT、会话等方式从服务器端获取用户的权限信息,然后挂载到req.user上。
内容的提问来源于stack exchange,提问作者gokcand

