关键操作防护实战:如何有效防止前端敏感操作被恶意触发
核心代码就这几行,但别小看它
上周我们上线了一个新功能:用户可以删除自己的项目。结果第二天就有用户反馈说“手滑点错了,项目没了”。我一看日志,确实——点击删除按钮后没任何确认,直接发请求删了。这哪行?赶紧加个二次确认。
但光弹个 confirm() 可不够。移动端上 confirm 体验差,而且没法自定义样式。更麻烦的是,有些操作根本不能靠前端拦截,比如通过浏览器开发者工具直接调用 API。所以得从“关键操作防护”这个角度系统性地做。
亲测有效的一套做法是:前端 + 后端双重校验。前端防误触,后端防绕过。下面先甩核心代码。
// 前端:带二次确认和防重复提交的删除操作
async function handleDelete(projectId) {
// 防重复点击
if (window.isDeleting) return;
window.isDeleting = true;
// 二次确认(用自定义弹窗,不是原生 confirm)
const confirmed = await showCustomConfirm('确定要删除该项目吗?此操作不可恢复。');
if (!confirmed) {
window.isDeleting = false;
return;
}
try {
const res = await fetch(https://jztheme.com/api/projects/${projectId}, {
method: 'DELETE',
headers: {
'X-CSRF-Token': getCSRFToken(), // 关键!防 CSRF
'Content-Type': 'application/json'
}
});
if (res.ok) {
showToast('删除成功');
refreshList();
} else {
throw new Error('删除失败');
}
} catch (err) {
console.error(err);
showToast('操作失败,请重试');
} finally {
window.isDeleting = false; // 无论成功失败都要释放锁
}
}
这段代码里藏着几个关键点,我一个个说。
踩坑提醒:这三点一定注意
第一,防重复提交别只靠按钮 disabled。我一开始以为把按钮设成 disabled 就行,结果用户狂点两下,发现还是发了两个请求。为啥?因为 disabled 是 UI 层的,JS 逻辑里如果没加锁,异步操作还没完成前,用户快速点击仍可能触发多次。所以必须用一个全局状态(比如 isDeleting)来硬拦截。虽然用 window 不优雅,但简单有效,项目紧急时建议直接用这种方式。
第二,CSRF Token 必须带上。很多团队只在表单里加 CSRF,但 AJAX 请求忘了。结果 DELETE、POST 这类操作被跨站伪造攻击。记住:只要是非 GET 请求,都必须带 CSRF Token。我们后端用的是 Laravel,默认会校验 X-CSRF-Token 头,所以前端每次请求都得塞进去。获取方式一般是页面渲染时写到 meta 标签里:
<meta name="csrf-token" content="{{ csrf_token() }}">
function getCSRFToken() {
return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
}
第三,二次确认不能只靠前端。有次测试同事直接用 Postman 调 DELETE 接口,绕过了前端确认,照样删了数据。所以后端必须再校验一次!比如要求传一个 confirm: true 参数,或者对高危操作单独走审批流程。我们现在的做法是:删除项目时,后端会检查用户是否在最近 5 分钟内访问过该项目详情页(通过 session 记录),没访问过就拒绝。这招防脚本批量删除很有效。
这个场景最好用:敏感操作加验证码
对于资金相关、账号注销这类超高危操作,光二次确认还不够。我建议直接上图形验证码或短信验证码。比如用户点“注销账号”,先弹验证码输入框,验证通过后再执行删除。
实现起来也不难:
async function handleAccountDeletion() {
const captcha = await promptCaptcha(); // 自定义弹窗,含验证码图片
if (!captcha) return;
const res = await fetch('/api/user/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ captcha })
});
if (res.status === 400) {
showToast('验证码错误');
return handleAccountDeletion(); // 递归重试,但要加次数限制
}
// ...后续处理
}
这里注意下,我踩过好几次坑:验证码接口要加频率限制,否则会被刷爆。另外,前端不要缓存验证码 ID,每次请求都该生成新的。曾经因为复用同一个 captcha_id,导致用户输一次验证码能删十次账号……
高级技巧:操作留痕 + 撤销窗口期
有些操作其实没必要立刻生效。比如删除项目,我们可以先标记为“待删除”,24 小时后再真正清除。这样用户后悔了还能找回。技术上就是加个 deleted_at 字段,定时任务清理。
前端配合也很简单:删除后提示“项目已移至回收站,7 天内可恢复”。用户真要彻底删,再点“清空回收站”——这时候才走高危操作流程(验证码+二次确认)。
另一个技巧是记录操作日志。每次关键操作,前端额外发个日志请求:
// 删除成功后
logSensitiveAction('project_delete', { projectId, timestamp: Date.now() });
后端收到后记到审计表里。出问题时能快速定位是谁、在什么时间、做了什么操作。这招在金融类项目里几乎是标配。
别被“完美方案”绑架,先跑起来再说
说实话,没有 100% 安全的方案。黑客真想搞你,总能找到漏洞。但我们的目标不是防住所有攻击,而是提高攻击成本,同时防止普通用户误操作。所以我的建议是:先上最简单的二次确认 + 防重复提交,再逐步加 CSRF、验证码、操作留痕。
改完后仍有一两个小问题,比如移动端弹窗偶尔遮挡按钮,但无大碍。安全防护是个持续过程,别指望一劳永逸。
以上是我踩坑后的总结,希望对你有帮助。这个技术的拓展用法还有很多(比如结合权限系统做动态防护),后续会继续分享这类博客。有更优的实现方式欢迎评论区交流。

暂无评论