如何在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
相关产品推荐
相关产品推荐

