直接用$_SESSION['token'] == $_POST['token']不安全,因未防时序攻击、未校验存在性与一致性、且未立即销毁token,导致重复提交可绕过;必须用hash_equals()校验并unset()销毁。

为什么直接用 $_SESSION['token'] == $_POST['token'] 不够安全
单纯比对 Session 中的 token 和 POST 提交的 token,无法阻止用户快速连点两次提交——因为第一次请求还没来得及 unset 或重生成 token,第二次请求就已抵达,两个请求看到的是同一个有效 token。
关键在于:token 必须「一次性」且「立即失效」。否则攻击者或误操作用户可能绕过校验。
- 每次成功验证后,必须立刻
unset($_SESSION['token'])或赋新值 - 不能等页面刷新后再清理,要放在表单处理逻辑的最前端(即验证通过后、业务写入前)
- 若业务逻辑抛异常或中途 exit,token 未被清理,会导致后续合法提交也被拒绝——需确保清理逻辑在 try-finally 或统一收口处执行
如何生成防重放、防预测的 token
用 bin2hex(random_bytes(32)) 而非 md5(uniqid()) 或 time().rand()。前者基于加密安全随机数生成器,后者可被预测或碰撞。
Session token 不需要持久化存储或数据库关联,但必须绑定当前用户会话($_SESSION 自然满足),且不暴露在 URL 中。
立即学习“PHP免费学习笔记(深入)”;
- 生成时机:在渲染表单前,调用
$_SESSION['form_token'] = bin2hex(random_bytes(32)); - 输出到表单:
<input type="hidden" name="token" value="= htmlspecialchars($_SESSION['form_token']) ?>"> - 禁止将 token 存入 cookie 或 localStorage 做二次传输——会破坏会话绑定性
POST 请求中验证 token 的标准流程
验证不是“有没有 token”,而是“token 是否存在、是否匹配、是否未被使用过”。三者缺一不可。
- 检查
$_POST['token']是否存在且非空,否则拒绝(防止绕过隐藏字段) - 检查
isset($_SESSION['form_token']),若不存在,说明会话丢失或已用过,拒绝 - 严格比对:
hash_equals($_SESSION['form_token'], $_POST['token'])(防时序攻击) - 验证通过后,立刻
unset($_SESSION['form_token'])—— 这步漏掉是重复提交最常见的原因
示例片段:
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!isset($_POST['token']) || !isset($_SESSION['form_token']) ||
!hash_equals($_SESSION['form_token'], $_POST['token'])) {
die('Invalid or expired token');
}
unset($_SESSION['form_token']); // ← 必须在这里,不是之后
// … 执行业务逻辑(如插入数据库)
}
AJAX 提交时 token 怎么同步更新
传统页面跳转下,表单页重新生成 token;但 AJAX 提交不刷新页面,下次提交仍用旧 token,必然失败。
解决方案不是“复用 token”,而是让 JS 在每次成功提交后,主动向服务端申请新 token,并更新表单字段值。
- 服务端提供轻量接口,如
/api/new-token,只返回json_encode(['token' => bin2hex(random_bytes(32))]),并写入$_SESSION - 前端在 AJAX success 回调里,用响应中的新 token 替换
$('input[name="token"]').val(...) - 禁止在 AJAX error 或 timeout 时更新 token——可能造成服务端已处理成功,但前端误以为失败而重发
这个环节最容易被忽略:前端没更新 token,后端已清空,第二次请求直接 403。



















