为何has_secure_password仅对新创建对象抑制更新时的密码验证?
has_secure_password与密码存在验证的行为解析
测试场景与现象
仅自定义密码存在验证
为password字段添加存在性验证:
class User < ApplicationRecord validates :password, presence: true end
尝试将密码更新为空值时,无论记录是刚创建还是从数据库加载,验证都会失败,行为符合预期:
cuser = User.create!(name: "Foo", password: "abc123") # 执行日志: # TRANSACTION (0.0ms) begin transaction # User Create (0.5ms) INSERT INTO "users" ("name", "password", "password_digest", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) RETURNING "id" [["name", "Foo"], ["password", "[FILTERED]"], ["password_digest", "[FILTERED]"], ["created_at", "2024-06-21 18:47:34.844263"], ["updated_at", "2024-06-21 18:47:34.844263"]] # TRANSACTION (0.1ms) commit transaction cuser.update!(name: "Bar", password: "") # 结果:Validation failed: Password can't be blank (ActiveRecord::RecordInvalid) fuser = User.last # 执行日志:User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ? [["LIMIT", 1]] fuser.update!(name: "Other", password: "") # 结果:Validation failed: Password can't be blank (ActiveRecord::RecordInvalid)
仅使用has_secure_password
class User < ApplicationRecord has_secure_password end
执行user.update!(name: "Bar", password: "")时,无论记录是刚创建还是从数据库加载都能成功,密码变更会被忽略:
cuser = User.create!(name: "Foo", password: "abc123") # 执行日志: # TRANSACTION (0.1ms) begin transaction # User Create (0.6ms) INSERT INTO "users" ("name", "password", "password_digest", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) RETURNING "id" [["name", "Foo"], ["password", "[FILTERED]"], ["password_digest", "[FILTERED]"], ["created_at", "2024-06-21 18:56:38.988813"], ["updated_at", "2024-06-21 18:56:38.988813"]] # TRANSACTION (0.1ms) commit transaction cuser.update!(name: "Bar", password: "") # 执行日志: # TRANSACTION (0.1ms) begin transaction # User Update (0.4ms) UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ? [["name", "Bar"], ["updated_at", "2024-06-21 18:56:55.334524"], ["id", 21]] # TRANSACTION (0.1ms) commit transaction fuser = User.last # 执行日志:User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ? [["LIMIT", 1]] fuser.update!(name: "Other", password: "") # 执行日志: # TRANSACTION (0.1ms) begin transaction # User Update (0.4ms) UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ? [["name", "Other"], ["updated_at", "2024-06-21 18:57:06.994792"], ["id", 21]] # TRANSACTION (0.1ms) commit transaction
同时使用两者
class User < ApplicationRecord has_secure_password validates :password, presence: true end
执行user.update!(name: "Bar", password: "")时,仅刚创建的记录能成功,从数据库加载的记录会验证失败:
cuser = User.create!(name: "Foo", password: "abc123") # 执行日志: # TRANSACTION (0.1ms) begin transaction # User Create (0.5ms) INSERT INTO "users" ("name", "password", "password_digest", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) RETURNING "id" [["name", "Foo"], ["password", "[FILTERED]"], ["password_digest", "[FILTERED]"], ["created_at", "2024-06-21 18:46:26.576676"], ["updated_at", "2024-06-21 18:46:26.576676"]] # TRANSACTION (1.2ms) commit transaction cuser.update!(name: "Bar", password: "") # 执行日志: # TRANSACTION (0.1ms) begin transaction # User Update (0.3ms) UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ? [["name", "Bar"], ["updated_at", "2024-06-21 18:46:26.579666"], ["id", 17]] # TRANSACTION (0.1ms) commit transaction fuser = User.last # 执行日志:User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ? [["LIMIT", 1]] fuser.update!(name: "Other", password: "") # 结果:Validation failed: Password can't be blank (ActiveRecord::RecordInvalid)
原因解析
这不是has_secure_password的bug,而是有意的设计特性,核心目的是平衡安全性与用户操作便利性:
仅使用
has_secure_password时:- 创建新记录时,会自动强制执行密码存在验证,确保新用户必须设置密码;
- 更新记录时,如果传入空密码(或nil),会直接忽略密码字段的更新逻辑——这是为了允许用户仅修改其他字段(如用户名、邮箱),而无需重复输入密码,提升操作体验。
同时添加手动存在验证时:
- 刚创建的内存记录(未从数据库重新加载):
has_secure_password会拦截空密码的赋值,将其识别为“不修改密码”的操作,因此跳过手动添加的存在验证,保证更新操作正常执行; - 从数据库加载的持久化记录:手动添加的存在验证会正常触发,避免用户通过传入空密码意外清空已设置的密码,兼顾安全性。
- 刚创建的内存记录(未从数据库重新加载):
这种设计既满足了创建用户时的密码必填要求,又允许更新时灵活修改其他字段,同时防止持久化记录的密码被意外清空。
内容的提问来源于stack exchange,提问作者Schwern
相关产品推荐
相关产品推荐

