直接禁用提交按钮不靠谱,因无法防止刷新、后退重发及绕过前端的请求;必须服务端校验唯一会话绑定token,生成用bin2hex(random_bytes(32)),校验需顺序检查存在性、时效性、一致性(hash_equals)并立即销毁。

为什么直接禁用提交按钮不靠谱
用户点一次就禁用按钮,看起来能防重复,但实际漏洞很多:刷新页面后按钮又可点了;后退再前进也可能触发;更别说爬虫或脚本绕过前端直接发请求。真正的防护必须落在服务端,且要和前端配合。
核心思路是:每次表单渲染时生成唯一 token,提交时校验它是否有效、未使用过、未过期。校验失败就拒绝处理,而不是靠前端“拦住”用户。
-
token必须绑定当前用户会话($_SESSION),不能全局共享 - 生成后立即存入
$_SESSION['form_token'],同时写进表单隐藏域:<input type="hidden" name="token" value="<?php echo $token; ?>"> - 每次提交后必须立刻销毁该
token(哪怕校验失败也要清掉,防止重放) - 不要用时间戳或自增 ID 当
token——它们可预测,容易被伪造
怎么生成安全可靠的 token
PHP 7.0+ 直接用 bin2hex(random_bytes(32)),短小、随机、无符号、不依赖外部扩展。别用 md5(time().rand()) 或 uniqid(),熵太低,有碰撞和预测风险。
示例代码片段:
立即学习“PHP免费学习笔记(深入)”;
$token = bin2hex(random_bytes(32)); $_SESSION['form_token'] = $token; $_SESSION['form_token_expire'] = time() + 600; // 10分钟过期
- 长度建议 ≥32 字节(64字符 hex),太短易爆破
- 存入
$_SESSION时顺带记个过期时间,后续校验先比时间再比值 - 如果用了 Redis 做 session 后端,确保序列化方式不破坏二进制数据(
igbinary或php_serialize都行,但别用默认的php处理 raw bytes)
提交时如何正确校验 token
重点不是“有没有 token”,而是“这个 token 是否属于本次会话、是否未过期、是否没被用过”。漏掉任一环节都可能被绕过。
校验逻辑顺序不能错:
- 检查
$_POST['token']是否存在且非空 - 检查
$_SESSION['form_token']是否存在 - 检查
$_SESSION['form_token_expire']是否 >=time() - 用
hash_equals()对比两个 token(防时序攻击),别用=== - 校验通过后,立刻执行
unset($_SESSION['form_token'], $_SESSION['form_token_expire'])
错误示例:if ($_POST['token'] === $_SESSION['form_token']) { ... } —— 这既不防时序攻击,也不清理已用 token,第二次提交照样能过。
多表单共存时 token 冲突怎么办
一个页面多个表单(比如资料编辑 + 密码修改 + 头像上传),共用一个 $_SESSION['form_token'] 就会互相覆盖,导致部分表单无法提交。
解决方法是给每个表单加命名空间:
- 生成时用键名区分:
$_SESSION['form_token_profile']、$_SESSION['form_token_password'] - 表单里对应写:
<input name="token_profile" value="<?php echo $profile_token; ?>"> - 校验时只操作对应键名,互不干扰
- 注意:不要把命名空间拼进 token 值本身(如
profile_abc123),否则 hash_equals 比对变复杂,还可能引入截断风险
真正麻烦的是 AJAX 表单 + 页面级 token 共存——比如弹窗表单需要单独获取 token,就得提供一个轻量接口(如 /api/token?form=comment),返回 JSON,前端注入到表单里。这时候 token 生命周期要更短(比如 2 分钟),且必须走 HTTPS。











