Rails 4 - Разрешить изменение пароля, только если текущий пароль правильный

В моем приложении пользователи могут редактировать информацию своего профиля. В форме редактирования профиля пользователь может вносить изменения во все поля (имя, должность и т. д.). В этой же форме есть три поля: current_password, password и password_confirmation. Я использую функцию has_secure_password bcrypt для аутентификации по паролю. Я вообще не использую Devise.

Я хочу, чтобы пользователи могли изменить свой пароль только в том случае, если они предоставили правильный текущий пароль. У меня это работало раньше со следующим кодом в методе update моего контроллера Users:

# Check if the user tried changing his/her password and CANNOT be authenticated with the entered current password
if !the_params[:password].blank? && [email protected](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? && [email protected](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 является виртуальным атрибутом в моей модели User:

attr_accessor :current_password

Я застрял, пытаясь понять это уже пару часов, так что мне действительно нужна помощь.

Спасибо!


Решение

Благодаря papirtiger у меня все получилось. Я немного изменил код из его ответа. Ниже мой код. Обратите внимание, что любой фрагмент кода будет работать нормально.

В модели пользователя (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

И код в моем контроллере Users теперь просто:

# 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

person Alexander    schedule 18.05.2015    source источник


Ответы (3)


Вы можете добавить пользовательскую проверку на уровне модели, которая проверяет, изменился ли пароль:

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
person max    schedule 18.05.2015
comment
Изменить: вы можете просто проверить, обновлен ли пароль. Я думаю, что дайджест создается при сохранении, что даст ложный отрицательный результат. - person max; 18.05.2015
comment
Спасибо за ответ. Да, дайджест дал ложноотрицательный результат. Несмотря на то, что отображалось сообщение об ошибке, модель все еще обновлялась. С этим новым кодом я получаю сообщение об ошибке: undefined method password_changed? - person Alexander; 18.05.2015
comment
Хм. Магия [attr]_changed? методы взяты из ActiveRecord::Dirty. Но я думаю, поскольку пароль является виртуальным атрибутом, он не отслеживается. Я не очень часто использовал has_secure_password, но я думаю, что вы могли бы просто проверить, равен ли пароль нулю. - person max; 18.05.2015
comment
Отредактировано, проверить на пустое? Вместо нуля? - person max; 18.05.2015
comment
Я удалил первый оператор IF из своего второго фрагмента кода. Я должен был это сделать? Когда я пытаюсь изменить свой пароль сейчас, проверка текущего пароля игнорируется, как и раньше. :/ Как вы предложили, я изменил свой код, чтобы проверить blank? - person Alexander; 18.05.2015
comment
Я соглашусь с двумя отдельными формами к завтрашнему дню (EST), если я не смогу понять это к тому времени. Часть меня думает, что мне, возможно, придется изменить дизайн опыта. В любом случае, я думаю, что слишком сильно беспокоился об этом аспекте моего сайта, особенно с учетом того, что мое приложение находится в альфа-версии: D - person Alexander; 18.05.2015
comment
Отредактировал ответ. Я протестировал это в образце приложения для рельсов, и оно соответствует вашим требованиям, но по какой-то странной причине authenticate(current_password) всегда возвращает false на моем сервере разработки. Когда я делаю это из консоли, он возвращает пользователя... - person max; 18.05.2015
comment
Снова отредактировано - в ActiveModel::SecurePassword есть какая-то странная ошибка, из-за которой аутентификация всегда возвращает false при вызове самого себя. Я смог подтвердить, что это работает так, как ожидалось. - person max; 18.05.2015
comment
Большое вам спасибо за вашу помощь; Я очень ценю это!! Это прекрасно работает! Я немного изменил ваш код, чтобы он соответствовал моему предпочтительному стилю, и внесу изменения в свой вопрос. - person Alexander; 19.05.2015

Итак, если подумать с точки зрения пользователя, если кто-то вводит неправильный пароль, разве вы не хотите, чтобы другие вещи также не менялись? Обычно у людей будет обновление пароля, где это просто адрес электронной почты и пароль. Если текущий пароль неверный, ничего не обновляйте.

Если вам нужно сделать это таким образом, просто переместите логику и установите два набора параметров или удалите пароль из параметров. Вот псевдокод для него.

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

#Normal update user logic
person Austio    schedule 18.05.2015
comment
Я тут как бы с вами не согласен. Меня довольно раздражают сервисы, которые требуют мой пароль для изменения моего профиля. И нет ничего плохого в том, чтобы сменить пароль в той же форме. - person max; 18.05.2015
comment
Спасибо за ответ. При необходимости я буду использовать две отдельные формы, как делал раньше. Однако я пытаюсь избежать этого, поскольку моя страница «Редактировать профиль» представляет собой интерфейс с вкладками. Одна из вкладок — смена пароля. Я чувствую, что пользователи изменят некоторые настройки на одной вкладке и перейдут на другую вкладку, прежде чем нажать «Сохранить». Они потеряют все свои изменения, если каждая вкладка представляет собой другую форму. Возможно, мне нужно пересмотреть свой дизайн. - person Alexander; 18.05.2015
comment
@papirtiger - это, безусловно, вопрос мнения. Если бы я увидел это в pr, мой комментарий был бы о том, что это добавляет некоторую вложенную сложность в эту форму, хотя этого не было бы, если бы это была отдельная вещь. Так что не обязательно неправильно, просто то, что нужно обсудить. Когда у меня есть выбор, я обычно делаю это в действии учетной записи, которое содержит только адрес электронной почты/пароль. - person Austio; 18.05.2015
comment
@ Александр, это понятно, надеюсь, это помогло вам встать на правильный путь. - person Austio; 18.05.2015

Другой подход заключается в использовании собственного валидатора вместо встраивания этой проверки в модель. Вы можете хранить эти пользовательские валидаторы в app/validators, и они будут автоматически загружены Rails. Я назвал это password_match_validator.rb.

Помимо многократного использования, эта стратегия также устраняет необходимость повторного запроса пользователя при аутентификации, поскольку экземпляр пользователя автоматически передается валидатору по рельсам в качестве аргумента «запись».

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 к каждой модели, вы можете объединить это с проблемой, но это, вероятно, излишне. Хорошо работает, если у вас есть отдельные модели для администратора и пользователя. Обратите внимание, что имя файла, имя класса И ключ, используемый в валидаторе, должны совпадать.

person John Saltarelli    schedule 01.11.2018
comment
Обновленный ответ, обнаружена ошибка, из-за которой, если вы используете аутентификацию (), она будет смотреть на последний пароль_дайджест вместо того, который сохраняется в БД. Поэтому нам нужно написать собственный метод внутри валидатора, который использует password_digest_was. - person John Saltarelli; 02.11.2018