CurrentPassword 必须是 Rule 类而非闭包,因闭包无法安全访问 Auth::user() 上下文、不可复用、难测试且不能注入依赖;Rule 类可在 passes() 中判空兜底、延迟取用户、支持类型提示,并覆盖未登录、空密码、用户密码为空等边界场景。

直接用 Rule::exists() 验证当前密码永远失败——数据库存的是哈希值,而用户输的是明文,字符串匹配不可能成立。必须用 Hash::check() 手动比对,且要封装成独立规则类,不能塞进闭包或控制器里硬写。
为什么 CurrentPassword 必须是 Rule 类,而不是闭包
闭包在 Form Request 或 $request->validate() 中无法访问 Auth::user() 的上下文生命周期,容易在未登录时触发 Trying to get property 'password' of non-object;而 Rule 类的 passes() 是延迟执行的,能安全拿到认证状态。更重要的是,闭包没法复用于 API 和 Web 两种场景,也没法被单元测试覆盖边界情况(比如空密码、$user 为 null)。
- 闭包里调
Auth::user()->password,未登录时直接报错崩溃 - Rule 类可在
passes()开头加if (! $user = Auth::user()) return false;安全兜底 - 测试时可 mock
Auth::shouldReceive('user')->andReturn(null)覆盖未登录分支 - 闭包无法注入依赖(如
Hash实例),Rule 类天然支持构造函数参数和类型提示
CurrentPassword 规则类的关键实现细节
生成后必须手动修改 app/Rules/CurrentPassword.php,重点不是“怎么写逻辑”,而是“怎么防崩”。passes() 里不能假设任何东西都存在。
- 第一行必须判
Auth::user()是否为空,否则未登录请求直接 500 - 第二步检查
$user->password是否为空字符串或 null,避免传给Hash::check()导致静默返回 false - 不建议在构造函数里查库或取用户,所有数据访问必须延迟到
passes() -
message()返回字符串即可,别拼接字段名,用:attribute占位符,中文名靠attributes()或语言文件映射
示例 passes() 片段:
public function passes($attribute, $value)
{
if (! $user = Auth::user()) {
return false;
}
if (empty($value) || ! $user->password) {
return false;
}
return Hash::check($value, $user->password);
}
在 Form Request 中使用时的常见踩坑点
规则名写对了,但验证总不触发?大概率是字段名或中间件没对齐。
- 前端传的字段名必须和规则数组 key 一致:如果表单发的是
old_password,规则就得写'old_password' => [new CurrentPassword],不能写成current_password - 控制器方法必须经过
auth或auth:api中间件,否则Auth::user()永远为 null,规则直接返回 false(这是预期行为,不是 bug) - 不要在
rules()里提前取$user = Auth::user()赋值给属性,Form Request 实例化早于认证中间件执行,此时$user一定是 null - 如果用了
withValidator()动态加规则,得确保它在认证中间件之后运行,否则同样拿不到用户
测试时最容易漏掉的三个边界场景
写完规则不写测试,等于没写。这三个 case 不覆盖,上线后用户会反馈“输什么密码都说错”,但日志里什么都没有。
- 未登录用户提交:确保
Auth::user()为 null 时规则返回 false,且不抛异常 - 提交空字符串或 null:
Hash::check('', '')返回 false,但最好显式拦截,避免后续逻辑误判 - 用户 password 字段为空字符串(比如迁移脚本清空过):
Hash::check()不报错但返回 false,需提前if (! $user->password)拦截并记录 warning 日志
真实项目中,第三个问题最隐蔽——它不会炸,但会让所有密码校验失败且无迹可寻。











