【问题标题】:Ruby on Rails update user attributes without passwordRuby on Rails 无需密码即可更新用户属性
【发布时间】:2021-06-04 11:10:49
【问题描述】:

我正在尝试为用户创建一种无需密码即可更新其详细信息的方法。我在我的用户模型中使用带有has_secure_password 的BCrypt,这样当用户注册或更改密码时,passwordpassword_confirmation 字段会在保存到password_digest 之前检查是否匹配。我也有以下设置密码的验证:

validates :password, presence: true, length: { minimum: 5 }

用户可以正常更新他们的密码(只要它通过至少 5 个字符长满足验证)。但是,如果用户尝试更新他们的详细信息(我有单独的视图/表单来更新密码和更新其他非必需属性),那么它会给出错误消息“密码不能为空,密码太短(最少 5 个字符) )"

我已经阅读了很多以前的 stackoverflow 问题/答案,我得到的最接近的是将, allow_blank: true 添加到验证的末尾。这几乎可以像我想要的那样工作,因为如果密码为空,BCrypt 会处理验证,因此在注册用户时不能提供空白密码,并且用户可以在没有密码的情况下更改他们的详细信息。但是,如果用户提供空白密码,则在更新密码时,会发生一些奇怪的事情。空白密码不会保存(因为它不应该因为 BCrypt 仍然需要匹配的密码和 password_confirmation,它不能为空),但是控制器的行为就像它一样并通过通知“密码已更新。”。这是我在控制器中的代码:

def update
  if Current.user.update(password_params)
    redirect_to root_path, notice: "Password updated."
  else
    render :edit
  end
end

private

def password_params
  params.require(:user).permit(:password, :password_confirmation)
end

我特别困惑为什么Current.user.update(password_params) 似乎返回true(因此重定向到根路径并传递前面提到的通知)但密码肯定没有更新。我已通过注销并使用以前的密码重新登录进行检查,但空白密码不允许我登录。

【问题讨论】:

    标签: ruby-on-rails validation passwords bcrypt


    【解决方案1】:

    在这种情况下,您最好的机会是访问has_secure_password 的源代码。您将在那里找到以下代码:

            define_method("#{attribute}=") do |unencrypted_password|
              if unencrypted_password.nil?
                self.public_send("#{attribute}_digest=", nil)
              elsif !unencrypted_password.empty?
                instance_variable_set("@#{attribute}", unencrypted_password)
                cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
                self.public_send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost))
              end
            end
    

    当您调用has_secure_password 时,此代码将创建设置器,其中attributepassword。它将动态创建以下方法:

            def password=(unencrypted_password)
              if unencrypted_password.nil?
                self.password_digest = nil
              elsif !unencrypted_password.empty?
                @password = unencrypted_password
                cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
                self.password_digest = BCrypt::Password.create(unencrypted_password, cost: cost))
              end
            end
    

    当你为你的模型分配属性时,这个方法会被调用:

    Current.user.update(password_params)
    

    如您所见,新定义的方法包含带有两个分支的简单条件:

    1. 如果您将password 设置为nil,它将删除password_digest
    2. 如果您将password 设置为非空字符串,它会将密码哈希存储到password_digest 属性

    就是这样。当您的用户发送空密码时,此方法决定不执行任何操作并忽略空字符串,它不会自动分配为空密码。模型几乎完全忽略了输入(如果password_confirmation 也是空的),没有任何内容被保存,没有违反验证,这意味着Current.user.update(password_params) 返回成功并且您的控制器继续“密码更新”。分支。

    现在,当您知道这种行为后,您能做些什么呢?


    假设你的模型看起来像这样

    class User < ApplicationRecord
      has_secure_password
      
      validates :password, presence: true, length: { minimum: 5 }
    end
    

    并且您希望您的验证在两种情况下都有效

    1. 创建新用户时
    2. 当用户更新密码时

    第一种情况很简单,正如您在问题中提到的,如果您使用allow_blank: true 而不是presence: true,则验证由has_secure_password 处理,如果密码为空,如果不是,则密码将继续验证。

    但是比我们得到的其他要求:

    • 用户必须能够更新其他属性,然后才能更新密码
    • 如果密码更新,我们仍然希望验证密码

    这两个要求排除了验证的presence: true 部分,它不可能存在。同样的事情也适用于allow_blank: true。在这种情况下,您可能想要使用条件验证:

    class User < ApplicationRecord
      has_secure_password
      
      validates :password, length: { minimum: 5 }, if: :password_required?
    
    private
    
      def password_required?
        password.present?
      end
    end
    

    此代码确保每次用户填写密码时都会执行验证。不管是create 还是update 操作。

    现在到最后一件事。

    如果用户留下空密码怎么办。 根据您的问题,我想如果用户发送空密码,您想显示验证错误。从上面的描述User 模型根本不知道它应该验证密码。你必须告诉你的模型,例如。像这样:

    class User < ApplicationRecord
      has_secure_password
      
      validates :password, length: { minimum: 5 }, if: :password_required?
    
      def enforce_password_validation
        @enforce_password_validation = true
      end
    
    private
    
      def password_required?
        @enforce_password_validation || password.present?
      end
    end
    

    现在您可以在控制器中像这样使用它:

      def update
        user = Current.user
        user.enforce_password_validation
    
        if user.update(password_params)
          redirect_to root_path, notice: "Password updated."
        else
          render :edit
        end
      end
    
    private
    
      def password_params
        params.require(:user).permit(:password, :password_confirmation)
      end 
    

    现在即使用户提交空密码,验证也会失败。

    【讨论】:

    • 哇,谢谢你这么详细的回答!这完全符合我的要求,并且解释非常有助于我理解发生了什么。
    • 太棒了,乐于助人。
    【解决方案2】:

    为了验证模型中的密码,请尝试以下方法:

    验证:密码,存在:真,长度:{最小值:5},开启::创建

    这行意味着验证将只调用 :create 方法而不是 :update 了。这样您就可以删除 allow_bank 并且您的更新密码可以正常工作

    【讨论】:

    • 不,这不起作用有两个原因。首先,它允许将密码更新为少于 5 个字符。其次,通知上写着“密码已更新”。输入空白密码后仍然显示。 (要清楚密码没有保存为空白,只有通知显示好像它有)。
    • 是的,这绝对很奇怪。你能试试这个Current.user.update!(password_params) 看看Update 方法是否会引发异常。只要把“!”像我一样在更新方法之后。
    猜你喜欢
    • 1970-01-01
    • 2011-10-28
    • 1970-01-01
    • 1970-01-01
    • 2014-04-14
    • 2014-02-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多