Rails 4 - 仅当当前密码正确时才允许更改密码

Posted

技术标签:

【中文标题】Rails 4 - 仅当当前密码正确时才允许更改密码【英文标题】:Rails 4 - Allow password change only if current password is correct 【发布时间】:2015-07-29 09:04:01 【问题描述】:

在我的应用中,用户可以编辑他们的个人资料信息。在编辑个人资料表单上,用户可以更改所有字段(姓名、职务等)。在同一个表单中包含三个字段:current_passwordpasswordpassword_confirmation。我正在使用bcrypthas_secure_password 功能进行密码验证。我根本没有使用设计。

我希望用户只有在提供了正确的当前密码后才能更改他们的密码。我之前在我的用户控制器的 update 方法中使用以下代码进行了此操作:

# Check if the user tried changing his/her password and CANNOT be authenticated with the entered current password
if !the_params[:password].blank? && !@user.authenticate(the_params[:current_password])
  # Add an error that states the user's current password is incorrect
  @user.errors.add(:base, "Current password is incorrect.")
else    
  # Try to update the user
  if @user.update_attributes(the_params)
    # Notify the user that his/her profile was updated
    flash.now[:success] = "Your changes have been saved"
  end
end

但是,这种方法的问题在于,如果当前密码不正确,它会丢弃对用户模型的所有更改。如果当前密码不正确,我想保存对用户模型的所有更改,但不保存密码更改。我试过像这样拆分 IF 语句:

# Check if the user tried changing his/her password and CANNOT be authenticated with the entered current password
if !the_params[:password].blank? && !@user.authenticate(the_params[:current_password])
  # Add an error that states the user's current password is incorrect
  @user.errors.add(:base, "Current password is incorrect.")
end

# Try to update the user
if @user.update_attributes(the_params)
  # Notify the user that his/her profile was updated
  flash.now[:success] = "Your changes have been saved"
end

这不起作用,因为即使当前密码不正确,用户也可以更改他/她的密码。单步执行代码时,虽然“当前密码不正确”。错误添加到@user,通过update_attributes方法运行后,似乎忽略了这个错误信息。

顺便说一句,current_password 字段是我的用户模型中的虚拟属性:

attr_accessor :current_password

我已经花了几个小时试图解决这个问题,所以我真的需要一些帮助。

谢谢!


解决方案

感谢papirtiger,我得到了这个工作。我从他的回答中稍微更改了代码。下面是我的代码。请注意,任一代码 sn-p 都可以正常工作。

在用户模型中 (user.rb)

class User < ActiveRecord::Base
  has_secure_password

  attr_accessor :current_password

  # Validate current password when the user is updated
  validate :current_password_is_correct, on: :update

  # Check if the inputted current password is correct when the user tries to update his/her password
  def current_password_is_correct
    # Check if the user tried changing his/her password
    if !password.blank?
      # Get a reference to the user since the "authenticate" method always returns false when calling on itself (for some reason)
      user = User.find_by_id(id)

      # Check if the user CANNOT be authenticated with the entered current password
      if (user.authenticate(current_password) == false)
        # Add an error stating that the current password is incorrect
        errors.add(:current_password, "is incorrect.")
      end
    end
  end
end

我的用户控制器中的代码现在很简单:

# Try to update the user
if @user.update_attributes(the_params)
  # Notify the user that his/her profile was updated
  flash.now[:success] = "Your changes have been saved"
end

【问题讨论】:

【参考方案1】:

所以从用户的角度考虑,如果有人输入了错误的密码,您是否不希望其他内容也不要更改?通常人们会有一个密码更新,它只是电子邮件和密码。如果当前密码不正确,则不要更新任何内容。

如果您必须这样做,那么只需移动逻辑并拥有两组参数或从参数中删除密码。这将是它的伪代码。

if not_authenticated_correctly
  params = params_minus_password_stuff (or use slice, delete, etc)
end

#Normal update user logic

【讨论】:

我有点不同意你的观点。我发现需要我的密码才能更改我的个人资料的服务很烦人。在同一个表单上更改密码并没有什么问题。 感谢您的回答。如果需要,我会像以前一样使用两个单独的表格。但是,我试图避免这种情况,因为我的编辑个人资料页面是一个选项卡式界面。选项卡之一是密码更改。我觉得用户会在一个选项卡中更改一些设置并在单击保存之前转到另一个选项卡。如果每个选项卡都是不同的表单,他们将丢失所有更改。也许我需要重新考虑我的设计。 @papirtiger - 这肯定是见仁见智。如果我在公关中看到这一点,我的评论将是它确实为这种形式增加了一些嵌套的复杂性,尽管如果它是一个单独的东西就不会存在。所以不一定是错的,只是需要讨论的事情。当我有选择时,我通常在仅包含电子邮件/密码的帐户操作中执行此操作。 @Alexander 可以理解,希望这能帮助您走上正轨。【参考方案2】:

您可以在模型级别添加自定义验证,以检查密码是否已更改:

class User < ActiveRecord::Base
  has_secure_password

  validate :current_password_is_correct,
           if: :validate_password?, on: :update

  def current_password_is_correct
    # For some stupid reason authenticate always returns false when called on self
    if User.find(id).authenticate(current_password) == false
      errors.add(:current_password, "is incorrect.")
    end
  end

  def validate_password?
    !password.blank?
  end

  attr_accessor :current_password
end

【讨论】:

编辑:您可以检查密码是否已更新。我认为摘要是在保存时生成的,这会产生误报。 感谢您的回答。是的,摘要确实给出了假阴性。即使显示错误消息,模型仍在更新。使用这个新代码,我收到一个错误:undefined method password_changed? 嗯。魔法[attr]_changed?方法来自ActiveRecord::Dirty。但我想因为密码是一个虚拟属性,而不是被跟踪。我没有经常使用 has_secure_password 但我认为您可以检查密码是否为零。 已编辑,检查是否空白?而不是零? 我已经从我的第二个代码 sn-p 中删除了第一个 IF 语句。我应该这样做吗?当我现在尝试更改密码时,会忽略当前密码的检查,就像以前一样。 :/ 正如你所建议的,我更改了代码以检查 blank?【参考方案3】:

另一种方法是使用自定义验证器,而不是将此验证嵌入模型中。您可以将这些自定义验证器存储在 app/validators 中,它们将由 Rails 自动加载。我称这个为password_match_validator.rb。

除了可重用之外,此策略还消除了在身份验证时重新查询 User 的需要,因为 User 实例通过 rails 作为“记录”参数自动传递给验证器。

class PasswordMatchValidator < ActiveModel::EachValidator

   # Password Match Validator
   #
   # We need to validate the users current password
   # matches what we have on-file before we change it
   #
   def validate_each(record, attribute, value)
     unless value.present? && password_matches?(record, value)
       record.errors.add attribute, "does not match"
     end
   end

   private

   # Password Matches?
   #
   # Need to validate if the current password matches
   # based on what the password_digest was. has_secure_password
   # changes the password_digest whenever password is changed.
   #
   # @return Boolean
   #
   def password_matches?(record, value)
     BCrypt::Password.new(record.password_digest_was).is_password?(value)
   end
 end

将验证器添加到项目后,您可以在任何模型中使用它,如下所示。

class User < ApplicationRecord

  has_secure_password

  # Add an accessor so you can have a field to validate
  # that is seperate from password, password_confirmation or 
  # password_digest...
  attr_accessor :current_password

  # Validation should only happen if the user is updating 
  # their password after the account has been created.
  validates :current_password, presence: true, password_match: true, on: :update, if: :password_digest_changed?

end

如果您不想将 attr_accessor 添加到每个模型中,您可以将其与关注点结合起来,但这可能是矫枉过正。如果您有针对管理员和用户的单独模型,则效果很好。请注意,文件名、类名和验证器上使用的密钥都必须匹配。

【讨论】:

更新了答案,发现了一个错误,如果您使用 authenticate(),它将查看“最新”密码摘要,而不是持久化到数据库的密码摘要。因此,我们需要在使用“password_digest_was”的验证器中编写自己的方法【参考方案4】:

刚刚发布,适用于 ror 6.x

form.erb 文件:

  <div class="field">
    <%= form.label :current_password, 'Current password:' %>
    <%= form.password_field :current_password, size: 40 %>
  </div>

  <div class="field">
    <%= form.label :password, 'Password:'%>
    <%= form.password_field :password, size:40 %>
  </div>

  <div class="field">
    <%= form.label :password_confirmation, 'Confirm:' %>
    <%= form.password_field :password_confirmation, id: :user_password_confirmation, size:40 %>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>

user.rb:

  has_secure_password

  # virtual attribute
  attr_accessor :current_password

  # Validate current password when the user is updated
  validate :current_password_is_correct, on: :update

  # Check if the inputted current password is correct when the user tries to update his/her password
  def current_password_is_correct
    # Check if the user tried changing his/her password
    return if password.blank?

    # Get a reference to the user since the "authenticate" method always returns false when calling on itself (for some reason)
    user = User.find(id)

    # Check if the user CANNOT be authenticated with the entered current password
    if user.authenticate(current_password) == false
      # Add an error stating that the current password is incorrect
      errors.add(:current_password, "is incorrect.")
    end
  end

users_controller.rb:

只需要在 def user_params 中添加 ":current_password" 否则将无法通过更改并在服务器日志中写入:

Unpermitted parameter: :current_password

【讨论】:

以上是关于Rails 4 - 仅当当前密码正确时才允许更改密码的主要内容,如果未能解决你的问题,请参考以下文章

centos7.6怎么设置允许新密码与用户名相同?

linux免密登录secure报密码过期

如果仅当当前字段存在于数组中时如何计算或求和字段

忘记密码rails 4.1

Ruby on Rails - 存储应用程序配置

为啥程序仅在 main 中定义时才给出输出(以及如何更改它)?