验证码不防重复提交,真正防重的是表单token机制;需结合session token生成销毁、PRG模式及数据库唯一索引三重保障。

验证码本身不防重复提交,它只负责校验人机身份;真正防止重复提交的是表单 token 机制。把验证码当成防重手段,是常见误解。
为什么验证码不能防重复提交
验证码(如图片验证码、短信验证码)的职责是确认操作者是真人,而非脚本或爬虫。它不绑定请求唯一性,也不控制请求幂等性:
- 用户输入正确验证码后,仍可手动刷新页面再提交一次——此时验证码已过期或被重用,但后端若没做 token 校验,照样写库
- 短信验证码常被用于登录/注册,但同一手机号+同一验证码可能被并发请求多次使用(尤其未加数据库唯一约束时)
- 图形验证码一般只校验一次,校验通过后服务端不会自动拒绝后续相同 POST 数据——除非你额外实现逻辑
PHP 表单防重必须用 $_SESSION['form_token'] + unset()
防重复提交的核心是「一次性凭证」,不是「人机验证」。PHP 中最可靠的做法是生成并销毁 session token:
- 表单页:调用
session_start()后生成bin2hex(random_bytes(16)),存入$_SESSION['form_token'],并输出到隐藏域<input type="hidden" name="token" value="<?php echo htmlspecialchars($token); ?>"> - 处理页:先比对
hash_equals($_SESSION['form_token'] ?? '', $_POST['token'] ?? ''),通过后立刻执行unset($_SESSION['form_token'])——这步漏掉,token 就变成永久有效的了 - 不要用
md5(time())或uniqid()生成 token,它们可预测,容易被绕过 - 若用户开两个标签页打开同一表单页,第二个提交会失败——这是设计使然,不是 bug;如需支持多页,得按业务拆分 token key,比如
$_SESSION['login_token']和$_SESSION['pay_token']
验证码 + 表单 token 必须配合 PRG 模式
仅校验 token 不够,还要避免用户刷新成功页导致重发 POST。必须走 POST-Redirect-GET 流程:
立即学习“PHP免费学习笔记(深入)”;
- 表单提交目标是处理脚本(如
submit.php),它完成校验、写库、unset($_SESSION['form_token'])后,立即header('Location: success.php') -
success.php只显示结果,不接收 POST,自然无法重复触发 - 跳转前没校验 token 就写库,或异常分支没跳转,都会让 PRG 失效
- ThinkPHP 等框架的
{__token__/}标签也依赖此流程,否则缓存页里的旧 token 会和 session 中已销毁的 token 不匹配
数据库层加 UNIQUE 索引是最后兜底
哪怕前端禁用按钮、后端 token 校验、PRG 跳转全做了,仍可能因网络超时、客户端重试、分布式 session 延迟等原因漏掉一次校验。这时靠数据库唯一约束收口最省事:
- 对业务天然唯一字段建索引,比如订单号、手机号+时间戳组合、用户ID+操作类型+日期
- 插入时捕获
PDOException,检查错误码是否为23000(SQLSTATE for integrity constraint violation) - 捕获到就返回“操作已存在”,而不是抛 500 或静默失败
- 别指望靠验证码字段本身加唯一索引——验证码是临时的、有时效的,且可能被多人同时正确输入
最容易被跳过的其实是 unset($_SESSION['form_token']) 这一步,尤其在多人协作或快速迭代时。token 生成了、传过去了、比对也写了,但销毁逻辑被注释、被遗漏、或放在了条件分支之外——结果就是整个防重链路形同虚设。



















