Express.js中使用multer搭配multipart/form-data时出现CSRF令牌错误
Express.js中Multer处理文件上传时CSRF令牌验证失败问题
问题场景
使用Express.js开发应用时,通过multer处理带文件上传的表单(表单设置enctype="multipart/form-data"),同时集成csurf中间件做CSRF保护,但提交表单时始终收到**"Invalid CSRF token"**错误。已确认表单中正确输出了<%= csrfToken %>,但验证依然失败。
问题代码
const express = require("express"); const app = express(); const dotenv = require("dotenv"); const expressLayouts = require("express-ejs-layouts"); const postRoutes = require("./routes/postRoutes"); const authRoutes = require("./routes/authRoutes"); const session = require("express-session"); const { isAuthenticated } = require("./middlewares/auth.middleware"); const pgSession = require("connect-pg-simple")(session); const csrf = require("csurf"); const flash = require("connect-flash"); const multer = require("multer"); const path = require("path"); dotenv.config(); // Load environment variables const port = process.env.PORT || 5000; // Sessions app.use( session({ store: new pgSession({ conObject: { host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASS, database: process.env.DB_NAME, port: process.env.DB_PORT, }, tableName: "sessions", }), secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: true, }) ); // Flash messages app.use(flash()); // Middleware to parse form data app.use(express.urlencoded({ extended: true })); app.use(express.json()); // Set up csrf protection middleware const csrfProtection = csrf(); app.use(csrfProtection); // Set variables in locals app.use((req, res, next) => { res.locals.isAuthenticated = req.session.userId ? true : false; res.locals.csrfToken = req.csrfToken(); res.locals.alerts = req.flash(); next(); }); app.use(express.static("public")); // Serve static files app.set("view engine", "ejs"); // Use EJS for templating app.use(expressLayouts); // Enable layouts app.set("layout", "layout"); // Multer setup const storage = multer.diskStorage({ destination: "./public/uploads/", filename: function (req, file, cb) { cb( null, file.fieldname + "-" + Date.now() + path.extname(file.originalname) ); }, }); const upload = multer({ storage: storage, limits: { fileSize: 10000000 }, fileFilter: function (req, file, cb) { const filetypes = /jpeg|png|jpg|gif/; const extname = filetypes.test(path.extname(file.originalname).toLowerCase()); const mimetype = filetypes.test(file.mimetype); if (mimetype && extname) { return cb(null, true); } else { cb("You can only upload image files"); } }, }); // Add post route with file upload app.post("/posts/add", upload.single("imageUrl"), async (req, res) => { const { title, content } = req.body; const userId = req.session.userId; const imageUrl = req.file ? `/uploads/${req.file.filename}` : null; try { await Posts.create({ title, content, imageUrl, userId }); res.redirect("/posts/me"); } catch (err) { console.error(err); req.flash("error", "Error adding post"); req.flash("oldTitle", title); req.flash("oldContent", content); req.flash("oldImageUrl", imageUrl || null); res.redirect("/posts/add"); } }); // Routes app.use("/posts", isAuthenticated, postRoutes); app.use("/auth", authRoutes); app.listen(port, () => console.log(`Server is running on http://localhost:${port}`) );
问题原因
核心矛盾在于:
express.urlencoded()中间件无法解析multipart/form-data格式的请求体,这类请求的解析依赖multer。- 当前代码中
csurf中间件是全局应用的,执行顺序在multer之前,此时请求体还未被解析,csurf无法从请求体中获取CSRF令牌,导致验证失败。
解决方案
方案1:调整路由级中间件顺序
移除全局的app.use(csrfProtection),仅在需要CSRF验证的路由中,先执行multer解析请求,再执行CSRF验证:
// 移除全局的app.use(csrfProtection) // 修改文件上传路由,先解析再验证 app.post("/posts/add", upload.single("imageUrl"), csrfProtection, async (req, res) => { const { title, content } = req.body; const userId = req.session.userId; const imageUrl = req.file ? `/uploads/${req.file.filename}` : null; try { await Posts.create({ title, content, imageUrl, userId }); res.redirect("/posts/me"); } catch (err) { console.error(err); req.flash("error", "Error adding post"); req.flash("oldTitle", title); req.flash("oldContent", content); req.flash("oldImageUrl", imageUrl || null); res.redirect("/posts/add"); } }); // 非文件上传的路由仍需全局或单独应用csrfProtection app.use("/auth", csrfProtection, authRoutes);
方案2:通过Cookie传递CSRF令牌
修改csurf配置,使其支持从Cookie中读取令牌,无需依赖请求体解析:
// 修改CSRF中间件配置 const csrfProtection = csrf({ cookie: true }); app.use(csrfProtection);
此方式下,表单中的CSRF令牌输出保持不变(<%= csrfToken %>),csurf会自动从Cookie或请求体中查找令牌,兼容文件上传场景。
额外注意事项
- 确保CSRF令牌的表单字段名为
_csrf(csurf默认字段名),若自定义字段名,需在配置中指定:csrf({ value: (req) => req.body.your_custom_field })。 - 确认
express-session中间件在csurf之前执行,csurf依赖会话存储令牌关联信息。
内容的提问来源于stack exchange,提问作者Sardorbek Shodmonov
相关产品推荐
相关产品推荐

