thinkphp密码重置必须守住三道防线:令牌用random_bytes()生成、存储用password_hash()哈希、验证需sql原子校验token_hash/used_at/expires_at;表结构须含token_hash、user_id、used_at、expires_at及联合唯一索引,且重置后须同步失效所有活跃会话。

ThinkPHP 实现密码重置接口,核心不是“加个路由写个方法”就能用,而是必须守住三道防线:令牌生成不可预测、存储必须哈希、验证必须原子且带状态约束。跳过任一环,就等于把重置入口直接暴露给自动化爆破。
password_reset 表字段设计必须包含哪些关键列
只存 token 和 user_id 是常见错误,会导致令牌复用、过期失效失效、无法审计。正确结构需强制包含:
-
token_hash:VARCHAR(255) NOT NULL,存password_hash($token, PASSWORD_ARGON2ID)结果,绝不能存明文或 md5 -
user_id:BIGINT/INT,外键关联用户表,建议设 ON DELETE CASCADE -
used_at:TIMESTAMP NULL,首次验证成功后立即UPDATE ... SET used_at = NOW() -
expires_at:TIMESTAMP NOT NULL,生成时设为date('Y-m-d H:i:s', time() + 1800)(30 分钟) - 联合唯一索引:
(token_hash, used_at),防止重复提交,也加速查询
生成 reset_token 必须用 random_bytes() 而非 uniqid() 或 md5()
用 uniqid()、md5(time().$email) 或 rand() 生成的 token 可被批量预测,实测在 1 秒内可穷举数万条有效链接。安全做法只有这一种:
php $token = bin2hex(random_bytes(32)); // 得到 64 位十六进制字符串 $token_hash = password_hash($token, PASSWORD_ARGON2ID); // 插入数据库时只存 $token_hash,$token 仅用于构造邮件链接
注意:random_bytes() 在 PHP 7.0+ 原生支持;若项目仍在 PHP 5.6,改用 openssl_random_pseudo_bytes(32) 并检查 $crypto_strong 返回 true。
验证 token 时 SQL 查询必须一次性完成状态与时间校验
典型错误是先查记录,再用 PHP 判断 $row['expires_at'] > date('Y-m-d H:i:s')——时区不一致、NTP 偏移、甚至 MySQL 服务器时间未同步都会导致绕过。必须让数据库自己判断:
sql SELECT id, user_id FROM password_resets WHERE token_hash = ? AND used_at IS NULL AND expires_at > NOW();
查到后立刻执行原子更新:
sql UPDATE password_resets SET used_at = NOW() WHERE id = ?;
如果这一步失败(如并发请求),后续所有操作都应中止并返回“令牌已使用”。另外,URL 中的 $_GET['t'] 务必 trim(),某些邮件客户端会在长 token 中自动插入 \r\n 折行。
重置密码后必须同步失效当前用户所有活跃会话
只更新 users.password 字段,旧 session 仍可继续登录,这是高危逻辑漏洞。ThinkPHP 中推荐两种处理方式:
- 若用数据库存 session(如
think_session表),执行DELETE FROM think_session WHERE user_id = ? - 若用文件或 Redis 存 session,需在用户表加
password_changed_at字段,中间件每次请求比对session_created_at ,不通过则清空 session 并跳转登录页
别忽略这个环节——它不是锦上添花,而是重置流程闭环的最后一块拼图。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











