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

如何在Express.js中防止页面刷新或点击返回按钮时的表单重复提交?刷新时需重新加载当前页面,返回时恢复页面之前的状态

这是Web开发里挺常见的痛点,尤其是在用Express做服务端渲染的场景下——用户刷新页面、点浏览器返回都可能触发重复提交,我之前做项目的时候也踩过类似的坑,给你分享几个实用的解决方案:

1. 用 Post/Redirect/Get (PRG) 模式解决刷新重复提交

这是最经典的方案,核心思路是:处理完表单POST请求后,不要直接返回页面,而是重定向到一个GET路由。这样用户刷新页面时,浏览器只会发送GET请求,不会重复执行POST的提交逻辑。

举个Express的例子:

// 处理表单提交的POST路由
app.post('/submit-form', (req, res) => {
  // 这里执行表单处理逻辑:比如存数据库、发送邮件等
  const formData = req.body;
  saveToDatabase(formData);

  // 用flash消息存提交成功的提示(需要先配置express-flash和session)
  req.flash('success', '表单提交成功!');
  // 重定向到结果页面或者原表单页面
  res.redirect('/form-page');
});

// 显示表单的GET路由
app.get('/form-page', (req, res) => {
  res.render('form', { successMsg: req.flash('success') });
});

这样用户提交后,浏览器地址栏会变成/form-page的GET地址,刷新的时候只会重新加载这个页面,不会重复提交表单。

2. 生成防重令牌,拦截重复提交(包括返回后再提交)

PRG能解决刷新问题,但如果用户点击浏览器返回按钮,回到之前的表单页面再点击提交,还是会重复发送POST请求。这时候需要给每个表单生成唯一的防重令牌:

步骤如下:

  • 在渲染表单的GET路由里,生成一个唯一令牌,存在用户的session中
  • 在表单里加入一个隐藏字段,把令牌传进去
  • 处理POST请求时,验证请求里的令牌和session里的是否一致,一致则处理请求并销毁令牌,不一致则拒绝提交

代码示例:

// 先配置session(必须的,因为要存令牌)
const session = require('express-session');
app.use(session({ secret: 'your-secret-key', resave: false, saveUninitialized: true }));

// 渲染表单的GET路由,生成防重令牌
app.get('/form-page', (req, res) => {
  // 生成唯一令牌,比如用uuid或者随机字符串
  const csrfToken = require('crypto').randomBytes(16).toString('hex');
  req.session.csrfToken = csrfToken;
  res.render('form', { csrfToken, successMsg: req.flash('success') });
});

// 处理表单提交的POST路由
app.post('/submit-form', (req, res) => {
  const { _csrf } = req.body;
  // 验证令牌
  if (!_csrf || _csrf !== req.session.csrfToken) {
    req.flash('error', '表单已提交,请不要重复操作!');
    return res.redirect('/form-page');
  }

  // 验证通过,销毁令牌(避免重复使用)
  delete req.session.csrfToken;

  // 执行表单处理逻辑
  const formData = req.body;
  saveToDatabase(formData);

  req.flash('success', '表单提交成功!');
  res.redirect('/form-page');
});

然后在你的表单模板(比如EJS)里加入隐藏字段:

<form method="POST" action="/submit-form">
  <input type="hidden" name="_csrf" value="<%= csrfToken %>">
  <!-- 其他表单字段 -->
  <button type="submit">提交</button>
</form>

这样用户返回旧表单页面时,令牌已经被销毁,再次提交会被拦截。

3. 控制页面缓存,避免返回旧表单

有时候浏览器会缓存表单页面,用户点击返回时显示的是缓存的旧页面,里面的令牌可能已经失效。这时候可以在渲染表单的GET路由里设置缓存控制头,让浏览器不缓存这个页面:

app.get('/form-page', (req, res) => {
  // 设置缓存控制头,禁止浏览器缓存
  res.set('Cache-Control', 'no-cache, no-store, must-revalidate');
  res.set('Pragma', 'no-cache');
  res.set('Expires', '0');

  const csrfToken = require('crypto').randomBytes(16).toString('hex');
  req.session.csrfToken = csrfToken;
  res.render('form', { csrfToken, successMsg: req.flash('success') });
});

这样用户返回时,浏览器会重新请求服务器,获取新的令牌和页面状态,避免使用缓存的旧表单。

4. 后端幂等性处理(最后一道防线)

前面的措施都是从前端/交互层面拦截,但为了绝对安全,后端接口要保证幂等性——同一个请求多次提交,结果是一样的,不会重复创建数据。

实现方式比如:

  • 给每个表单提交生成一个唯一的业务标识(比如用UUID生成requestId,放在表单隐藏字段里)
  • 处理请求时,先检查数据库里是否已经存在这个requestId对应的记录
  • 如果存在,直接返回成功(或者提示已提交);如果不存在,再执行处理逻辑

代码示例:

app.post('/submit-form', (req, res) => {
  const { _csrf, requestId } = req.body;

  // 先验证防重令牌
  if (!_csrf || _csrf !== req.session.csrfToken) {
    req.flash('error', '表单已提交,请不要重复操作!');
    return res.redirect('/form-page');
  }

  // 检查requestId是否已存在
  const exists = checkIfRequestExists(requestId);
  if (exists) {
    req.flash('error', '该表单已提交过!');
    return res.redirect('/form-page');
  }

  // 处理逻辑
  saveToDatabase({ ...req.body, requestId });
  delete req.session.csrfToken;

  req.flash('success', '表单提交成功!');
  res.redirect('/form-page');
});

表单里要加入requestId字段:

<form method="POST" action="/submit-form">
  <input type="hidden" name="_csrf" value="<%= csrfToken %>">
  <input type="hidden" name="requestId" value="<%= require('crypto').randomBytes(16).toString('hex') %>">
  <!-- 其他字段 -->
</form>
总结

建议把这些方案结合起来用:

  • PRG模式解决刷新重复提交的问题
  • 防重令牌拦截返回后再提交的情况
  • 缓存控制避免返回旧表单
  • 后端幂等性作为兜底,确保数据安全

内容的提问来源于stack exchange,提问作者Shreya Sheel Sachan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.28 17:33:11