модуль ActiveModel::SecurePassword::ClassMethods
Открытые методы экземпляра
# File activemodel/lib/active_model/secure_password.rb, line 125
def has_secure_password(attribute = :password, validations: true, reset_token: true)
# Load bcrypt gem only when has_secure_password is used.
# This is to avoid ActiveModel (and by extension the entire framework)
# being dependent on a binary library.
begin
require "bcrypt"
rescue LoadError
warn "You don't have bcrypt installed in your application. Please add it to your Gemfile and run bundle install."
raise
end
include InstanceMethodsOnActivation.new(attribute, reset_token: reset_token)
if validations
include ActiveModel::Validations
# This ensures the model has a password by checking whether the password_digest
# is present, so that this works with both new and existing records. However,
# when there is an error, the message is added to the password attribute instead
# so that the error message will make sense to the end-user.
validate do |record|
record.errors.add(attribute, :blank) unless record.public_send("#{attribute}_digest").present?
end
validate do |record|
if challenge = record.public_send(:"#{attribute}_challenge")
digest_was = record.public_send(:"#{attribute}_digest_was") if record.respond_to?(:"#{attribute}_digest_was")
unless digest_was.present? && BCrypt::Password.new(digest_was).is_password?(challenge)
record.errors.add(:"#{attribute}_challenge")
end
end
end
# Validates that the password does not exceed the maximum allowed bytes for BCrypt (72 bytes).
validate do |record|
password_value = record.public_send(attribute)
if password_value.present? && password_value.bytesize > ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
record.errors.add(attribute, :password_too_long)
end
end
validates_confirmation_of attribute, allow_nil: true
end
# Only generate tokens for records that are capable of doing so (Active Records, not vanilla Active Models)
if reset_token && respond_to?(:generates_token_for)
reset_token_expires_in = reset_token.is_a?(Hash) ? reset_token[:expires_in] : DEFAULT_RESET_TOKEN_EXPIRES_IN
silence_redefinition_of_method(:"#{attribute}_reset_token_expires_in")
define_method(:"#{attribute}_reset_token_expires_in") { reset_token_expires_in }
generates_token_for :"#{attribute}_reset", expires_in: reset_token_expires_in do
public_send(:"#{attribute}_salt")&.last(10)
end
class_eval <<-RUBY, __FILE__, __LINE__ + 1
silence_redefinition_of_method :find_by_#{attribute}_reset_token
def self.find_by_#{attribute}_reset_token(token)
find_by_token_for(:#{attribute}_reset, token)
end
silence_redefinition_of_method :find_by_#{attribute}_reset_token!
def self.find_by_#{attribute}_reset_token!(token)
find_by_token_for!(:#{attribute}_reset, token)
end
RUBY
end
end Добавляет методы для задания пароля BCrypt и проверки подлинности с его помощью. Для этого механизма требуется атрибут XXX_digest, где XXX — это имя атрибута, предназначенного для хранения пароля.
Автоматически добавляются следующие проверки:
-
При создании пароль должен быть задан
-
Длина пароля должна быть не более 72 байт
-
Подтверждение пароля (с использованием атрибута
XXX_confirmation)
Если проверка подтверждения не нужна, просто не указывайте значение для XXX_confirmation (то есть не добавляйте для него поле формы). Если значение этого атрибута равно nil, проверка выполняться не будет.
Кроме того, создается атрибут XXX_challenge. Если ему присвоено значение, отличное от nil, он будет проверен на соответствие текущему сохраненному паролю. Эта проверка использует отслеживание изменений, предоставляемое ActiveModel::Dirty; если методы отслеживания изменений не определены, проверка завершится неудачей.
Все перечисленные выше проверки можно отключить, передав аргумент validations: false. Это позволяет полностью настроить поведение проверок.
Токен сброса пароля (по умолчанию действительный в течение 15 минут) настраивается автоматически, если для reset_token задано значение true (по умолчанию это так) и объект отвечает на generates_token_for (как это происходит у Active Record).
Наконец, срок действия токена сброса можно настроить, передав хеш в has_secure_password:
has_secure_password reset_token: { expires_in: 1.hour }
Чтобы использовать has_secure_password, добавьте bcrypt (~> 3.1.7) в свой Gemfile:
gem "bcrypt", "~> 3.1.7"
Примеры
Использование Active Record (который автоматически подключает ActiveModel::SecurePassword)
# Schema: User(name:string, password_digest:string, recovery_password_digest:string)
class User < ActiveRecord::Base
has_secure_password
has_secure_password :recovery_password, validations: false
end
user = User.new(name: "david", password: "", password_confirmation: "nomatch")
user.save # => false, password required
user.password = "vr00m"
user.save # => false, confirmation doesn't match
user.password_confirmation = "vr00m"
user.save # => true
user.authenticate("notright") # => false
user.authenticate("vr00m") # => user
User.find_by(name: "david")&.authenticate("notright") # => false
User.find_by(name: "david")&.authenticate("vr00m") # => user
user.recovery_password = "42password"
user.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uXkTG65HR.qpW.bNhEjFP3ftli3o5DQC"
user.save # => true
user.authenticate_recovery_password("42password") # => user
user.update(password: "pwn3d", password_challenge: "") # => false, challenge doesn't authenticate
user.update(password: "nohack4u", password_challenge: "vr00m") # => true
user.authenticate("vr00m") # => false, old password
user.authenticate("nohack4u") # => user
Условное требование пароля
class Account
include ActiveModel::SecurePassword
attr_accessor :is_guest, :password_digest
has_secure_password
def errors
super.tap { |errors| errors.delete(:password, :blank) if is_guest }
end
end
account = Account.new
account.valid? # => false, password required
account.is_guest = true
account.valid? # => true
Использование токена сброса пароля
user = User.create!(name: "david", password: "123", password_confirmation: "123") token = user.password_reset_token User.find_by_password_reset_token(token) # returns user # 16 minutes later... User.find_by_password_reset_token(token) # returns nil # raises ActiveSupport::MessageVerifier::InvalidSignature since the token is expired User.find_by_password_reset_token!(token)
© 2004–2021 David Heinemeier Hansson
Licensed under the MIT License.