You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 12:31:05