Я использую драгоценный камень Bcrypt для шифрования паролей.
Модель пользователя:
class User < ApplicationRecord
has_secure_password
validates :password, presence: true, length: { minimum: 6 }, confirmation: true
# ...
end
Как сделать так, чтобы пароль был пустым при обновлении пользователя?:
expect(user.update(name: 'Joana', password: '')).to be(true)
Но не обновить пароль?
expect { user.update(name: 'Joana', password: '') }
.not_to(change { user.reload.password_digest })
Если вы хотите протестировать его, не забудьте найти пользователя перед обновлением, чтобы не получить ложных срабатываний:
user = User.find(user.id)
Рассматривали ли вы проверку, которая гарантирует наличие password
?
Да, я это сделал, но это не позволяет мне сохранять другие атрибуты в операторе обновления, если пароль отсутствует. @spickermann
Хорошо, я могу очистить его в контроллере или во внешнем интерфейсе. Тогда мне следует добавить allow_nil: { on: :update }
к проверке пароля? @Стефан
Если пользователь не хочет устанавливать новый пароль, интерфейсная часть, вероятно, не должна отправлять пустое значение. Кроме того, вызов params.compact_blank!
в вашем контроллере приведет к удалению пустых значений.
это чисто стилистическая проблема, но я бы удалил поле пароля, если оно пустое, в обратном вызове before_update в модели, а не при манипуляциях в контроллере. Это парадигма «толстой модели и тощего контроллера». На мой взгляд, контроллеры слишком перегружены.
Да, я думал об этом. Однако я не знаю, как это сделать. Вот почему я задал вопрос. @LesNightingill
Я справляюсь с этим в своем приложении следующим образом. Этой части моего кода уже несколько лет, поэтому в более поздних версиях Rails могут быть некоторые улучшения, но она работает нормально.
Конечно, вы шифруете пароль перед сохранением в базе данных. Допустим, поле базы данных, в котором хранится зашифрованный пароль, — crypted_password
.
Теперь в вашей модели User
у вас есть:
class User < ApplicationRecord
attr_accessor :password # this is the name of the parameter that comes in from the form on the front end
before_save :encrypt_password
def encrypt_password
return if password.blank? # return early and don't assign any value to crypted_password, and it will not be changed
self.salt = Digest::SHA1.hexdigest("--#{Time.now.to_s}--#{login}--") if salt.blank?
self.crypted_password = encrypt(password) # encrypt, here, is a method provided by my included authentication module
end
end
Я использую драгоценный камень Bcrypt, поэтому, возможно, для меня это будет совсем по-другому.
ОБНОВЛЕНИЕ Теперь, когда мы увидели реальную модель пользователя...
has_secure_password уже работает так, как вы описываете. Он добавляет кучу собственных проверок и игнорирует пустой пароль.
class User < ApplicationRecord
has_secure_password
validates :password,
presence: true,
length: { minimum: 6 },
confirmation: true
end
И несколько быстрых проверок в консоли Rails.
3.2.2 :001 > user = User.create!(name: "Foo", password: "abc123")
TRANSACTION (0.1ms) begin transaction
User Create (0.7ms) 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-20 19:09:14.107560"], ["updated_at", "2024-06-20 19:09:14.107560"]]
TRANSACTION (1.1ms) commit transaction
=>
#<User:0x00000001096d28d8
...
3.2.2 :002 > user.update!(name: 'Joana', password: '')
TRANSACTION (0.1ms) begin transaction
User Update (0.4ms) UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ? [["name", "Joana"], ["updated_at", "2024-06-20 19:09:17.852613"], ["id", 6]]
TRANSACTION (0.1ms) commit transaction
=> true
3.2.2 :003 > user.update!(name: 'Joana', password: '')
=> true
3.2.2 :004 > user.update!(name: 'Joana', password: nil)
(irb):4:in `<main>': Validation failed: Password can't be blank, Password can't be blank, Password is too short (minimum is 6 characters) (ActiveRecord::RecordInvalid)
user.update!(name: 'Joana', password: '')
обновляет только имя, а не пароль. Это функция, предоставленная has_secure_password
.
class User
before_update -> { self.restore_password! if self.password.blank? }
...
end
Однако обратные вызовы моделей могут вызвать проблемы, особенно когда они обеспечивают соблюдение «бизнес-правил», которые могут измениться или не быть универсально применимыми. А иногда обратные вызовы пропускаются и могут усложнить массовое обновление базы данных. Это проблема «толстой модели».
Вместо этого сделайте это как проверку модели.
class User
validates :password, presence: true
...
end
Обычно вы оставляете все как есть и используете проверку, чтобы сообщить пользователю о проблеме.
Если вы хотите обновить другие атрибуты пользователя, даже если один из них недействителен, это сомнительная практика, вам следует удалить пустой параметр пароля в вашем контроллере. Лучшая причина — если пользователь случайно вставит пробел в поле пароля или что-то в этом роде.
class UserController
...
def update
# or `params.compact_blank!` to do this for all parameters
params.delete(:password) if params[:password].blank?
...
end
end
Но не изобретайте заново учетные записи пользователей. Используйте придумать . Он безопасен, хорошо документирован и работает со многими другими драгоценными камнями.
Что ж, я делаю API с JSON Web Token. Девайс очень хорошо работает в монолите. Однако все API-решения с devise, которые я видел до сих пор, либо не поддерживаются, либо требуют столько же работы, как и отсутствие использования devise вообще.
С другой стороны, если я удалю пароль из оператора обновления user.update(name: 'Joana')
, проверка присутствия все равно завершится неудачей. Поэтому мне также приходится разрешить ноль при проверке пароля при обновлении, что кажется нелогичным.
@ChiaraAni Ты пробовал? user.update!(password: nil)
, user.update!(password: "")
и user.update!(password: " ")
не пройдут проверку, но user.update(name: 'Joana')
работает нормально, поскольку пароль не обновляется. Вам не нужно разрешать ноль. Возможно, недоразумение здесь в том, что user.update(name: "Foo")
и user.update(name: "Foo", password: nil)
разные. Существует очень важная разница между передачей ключа со значением nil и отсутствием передачи ключа вообще.
@ChiaraAni Я обновил свой ответ теперь, когда вы опубликовали свою модель пользователя. Это уже работает, как вы описали. Вы изменили свою модель пользователя?
Прежде чем обновлять его, не забудьте найти пользователя так, как будто вы находитесь в контроллере: User.find(user.id)
. Ваш подход является ложным срабатыванием. Похоже, он получает пароль из рубиновой памяти, поэтому он не дает сбоя, как должен.
@ChiaraAni Ты прав! Это странно. Между объектами есть внутренние различия, но для проверки это не имеет значения. Нет состояний «создать публикацию» или «найти публикацию». Возможно, вы нашли ошибку. Еще одна причина не полагаться на этот метод и вместо этого очистить свои входные данные.
@ChiaraAni Я думаю, что это ошибка или недоработка has_secure_password
. ‼Я задала по этому поводу фокусированный вопрос.
class User < ApplicationRecord
has_secure_password
validates(
:password,
allow_nil: { on: :update }, # add this
length: { minimum: 6 }
)
# ...
end
Таким образом, эти ожидания оправдываются:
user = User.find(user.id) # Refresh object as if you were in controller
expect(user.update(name: 'Joana')).to be(true)
expect(user.update(name: 'Joana', password: '')).to be(true)
expect { user.update(name: 'Joana', password: '') }
.not_to(change { user.reload.password_digest })
Однако имейте в виду, что он не будет обновляться с фактическим паролем nil
:
expect(user.update(name: 'Joana', password: nil)).to be(false)
Итак, это противоречит здравому смыслу, но это работает.
Подтверждение и проверка присутствия не требуются, поскольку они предоставляются Rails.
Вы получите тот же результат с allow_nil
или без него. Демонстрация. И ваш вопрос был не о нуле, обновите свой вопрос, пожалуйста.
Я думаю, у вас ложноположительный результат. Если вы действительно найдете пользователя User.find(user.id)
, а затем обновите его, ничего не получится. Поэтому он терпит неудачу, когда вы обновляете его в контроллере. Правда, мой вопрос был не о nil
, и я не хочу, чтобы он был о nil
, я просто имею в виду, что это нелогично, что с nil
это не работает. @Шверн
Лучшим способом протестировать это было бы попытаться обновить пользователя, а затем вместо этого использовать expect(user.authenicate(old_password)).to be_truthy
. Столбец дайджеста пароля — это внутренняя деталь реализации, о которой ваши тесты не обязательно должны знать.
Если вы не хотите обновлять значение, не передавайте его в оператор обновления.