最稳妥做法是在 rules() 中不设 password 为 safe,更新前用 setattributes($data, false) 跳过非 safe 字段;修改密码需单独处理并清空明文;beforesave() 中仅对非空且已变更的明文密码加密。

更新时跳过密码字段的自动赋值
Yii2 的 ActiveRecord::update() 默认会把所有属性都写入 SQL,如果密码字段被用户提交了明文(比如表单里没传值但模型仍带空字符串),直接更新就会覆盖掉已加密的密码。最稳妥的做法是:在 rules() 里不给密码字段加 safe,同时在更新前手动排除它。
常见错误现象:UPDATE user SET password='', updated_at=... WHERE id=123 —— 密码被清空了。
- 在模型的
rules()中,不要为password字段声明safe或required(除非是注册场景) - 更新前调用
$model->setAttributes($postData, false),第二个参数false表示只赋值safe属性,自然跳过password - 如果必须接收前端传来的密码(比如“修改密码”页面),应单独处理:
if (!empty($postData['password'])) { $model->setPassword($postData['password']); }
使用 beforeSave() 自动加密非空密码
把加密逻辑收口到 beforeSave() 是最符合 Yii2 惯例的方式。它能确保无论通过 save()、update() 还是 updateAll()(注意后者不触发此钩子)写入,只要走模型流程,密码就自动处理。
容易踩的坑:beforeSave() 在插入和更新时都会触发,但你通常只想在密码有变化时才重加密;否则每次更新其他字段(如邮箱)也会无谓地重新哈希一次。
- 判断是否为新记录:
if ($this->isNewRecord),此时必须加密 - 判断密码字段是否被修改:
if ($this->isAttributeChanged('password') && !empty($this->password)) - 加密建议用
Yii::$app->getSecurity()->generatePasswordHash($this->password),别手写password_hash() - 加密后记得清空明文:
$this->password = '',防止意外泄露或重复哈希
批量更新时密码字段完全绕过模型层
ActiveRecord::updateAll() 和原生 SQL 更新不会调用 beforeSave() 或验证规则,密码字段一旦出现在 $attributes 数组里,就会被原样写入数据库 —— 这是硬伤,无法靠模型自动修复。
典型场景:后台管理员批量启用/禁用用户,顺手把 password 也塞进更新数组里,结果全变空字符串或明文。
- 绝对不要在
updateAll()的$attributes参数中包含password - 如果真要批量重置密码,单独执行:
User::updateAll(['password' => Yii::$app->getSecurity()->generatePasswordHash('temp123')], ['id' => $ids]) - 更安全的做法是封装成服务方法,禁止外部直接调用
updateAll()操作敏感字段
验证密码变更时避免二次哈希
有些开发者会在 beforeSave() 里无条件执行 $this->password = Yii::$app->getSecurity()->generatePasswordHash($this->password),但如果用户没改密码,而数据库里存的是哈希值,这就导致哈希值被当明文再哈希一次 —— 登录永远失败。
关键点在于区分“用户输入的明文密码”和“数据库里已存的哈希值”。Yii2 不提供自动识别机制,得自己守好边界。
- 始终假设
$this->password是用户刚提交的明文(或空),不是数据库读出来的值 - 不要在
afterFind()里对password做任何解密或转换 —— Yii2 的密码字段就该只进不出 - 登录验证必须用
Yii::$app->getSecurity()->validatePassword($input, $hash),而不是自己比对字符串
null 在数据库层面可能被不同处理,尤其当字段定义为 NOT NULL 时,'' 会被存进去,后续 validatePassword() 直接返回 false,连调试日志都看不出问题。











