安全生成密码重置链接需使用random_bytes(32)生成令牌并bin2hex()存储,配reset_requested_at时间戳,独立于邮箱验证令牌;校验时先查匹配且未过期的reset_token,用hash_equals防时序攻击,通过后立即清空令牌并刷新。

密码重置链接怎么生成才安全
不能直接用 $user->getId() 或邮箱拼 URL,否则容易被枚举或重放。正确做法是生成一次性、带签名、有时效的令牌。
推荐用 random_bytes(32) 生成原始令牌,再用 bin2hex() 转成字符串存库(字段名建议叫 reset_token),同时记录 reset_requested_at 时间戳。
- 生成后必须调用
$entityManager->flush(),否则邮件里发出去的令牌在数据库里查不到 - 链接用
$urlGenerator->generate('app_reset_password', ['token' => $token])构造,别手拼 URL - 发完邮件立刻清空 session 中可能缓存的旧 token,避免用户重复点击触发多次发送
邮箱验证和密码重置共用一套令牌机制?
不建议共用。两者语义、生命周期、使用场景完全不同:邮箱验证是注册后一次性的身份确认,密码重置是用户主动发起的敏感操作,且可能高频触发。
混用会带来风险:
- 重置令牌被误用于验证邮箱,绕过注册流程
- 验证令牌过期时间设长(比如 7 天),但重置令牌应更短(2 小时),无法统一配置
- 数据库字段语义混乱,后续加审计或清理逻辑时容易出错
正确做法是分两张表或至少两个字段:confirmation_token + reset_token,各自独立生成、校验、失效。
点击重置链接后怎么校验并允许改密
路由接收 token 参数后,第一步不是查用户,而是查 reset_token 字段是否匹配 + 是否未过期(reset_requested_at >= now - 2 hours)。
- 查不到或过期,直接返回 404,不暴露该 token 对应的邮箱是否存在
- 匹配成功后,用
hash_equals($expected, $provided)比对,防时序攻击 - 校验通过立即清空
reset_token和reset_requested_at,再$entityManager->flush() - 此时才能把用户对象传给密码修改表单——注意这个用户还不能直接登录,要等新密码设完才重新认证
为什么 resetRequestedAt 字段必须设为 nullable
因为不是所有用户都会触发密码重置,这个字段只在请求重置时写入。设为 nullable=true 是数据库设计基本要求,否则插入新用户时会因非空约束失败。
更关键的是查询逻辑依赖它:
-
SELECT * FROM user WHERE reset_token = ? AND reset_requested_at IS NOT NULL—— 这样能排除掉历史残留的空值干扰 - 命令行清理脚本(如每天删掉 2 小时前的未使用重置请求)也靠它判断有效性
- 如果误设为
NOT NULL默认值0000-00-00 00:00:00,会导致所有新用户默认带上无效时间戳,后续查询全错
字段类型推荐 datetime(MySQL)或 datetime_immutable(Doctrine 类型),别用字符串存时间。


















