未验证 提交 08dde0f3 编写于 作者: R Rafael Mendonça França

Merge pull request #26764 from choncou/improve_has_secure_password

Allow configurable attribute name on `#has_secure_password`
* Allows configurable attribute name for `#has_secure_password`. This
still defaults to an attribute named 'password', causing no breaking
change. There is a new method `#authenticate_XXX` where XXX is the
configured attribute name, making the existing `#authenticate` now an
alias for this when the attribute is the default 'password'.
Example:
class User < ActiveRecord::Base
has_secure_password :activation_token, validations: false
end
user = User.new()
user.activation_token = "a_new_token"
user.activation_token_digest # => "$2a$10$0Budk0Fi/k2CDm2PEwa3Be..."
user.authenticate_activation_token('a_new_token') # => user
*Unathi Chonco*
* Add `config.active_model.i18n_full_message` in order to control whether
the `full_message` error format can be overridden at the attribute or model
level in the locale files. This is `false` by default.
......
......@@ -16,15 +16,16 @@ class << self
module ClassMethods
# Adds methods to set and authenticate against a BCrypt password.
# This mechanism requires you to have a +password_digest+ attribute.
# This mechanism requires you to have a +XXX_digest+ attribute.
# Where +XXX+ is the attribute name of your desired password/token or defaults to +password+
#
# The following validations are added automatically:
# * Password must be present on creation
# * Password length should be less than or equal to 72 bytes
# * Confirmation of password (using a +password_confirmation+ attribute)
# * Confirmation of password (using a +XXX_confirmation+ attribute)
#
# If password confirmation validation is not needed, simply leave out the
# value for +password_confirmation+ (i.e. don't provide a form field for
# If confirmation validation is not needed, simply leave out the
# value for +XXX_confirmation+ (i.e. don't provide a form field for
# it). When this attribute has a +nil+ value, the validation will not be
# triggered.
#
......@@ -37,9 +38,10 @@ module ClassMethods
#
# Example using Active Record (which automatically includes ActiveModel::SecurePassword):
#
# # Schema: User(name:string, password_digest:string)
# # Schema: User(name:string, password_digest:string, activation_token_digest:string)
# class User < ActiveRecord::Base
# has_secure_password
# has_secure_password :activation_token, validations: false
# end
#
# user = User.new(name: 'david', password: '', password_confirmation: 'nomatch')
......@@ -48,11 +50,15 @@ module ClassMethods
# user.save # => false, confirmation doesn't match
# user.password_confirmation = 'mUc3m00RsqyRe'
# user.save # => true
# user.activation_token = "a_new_token"
# user.activation_token_digest # => "$2a$10$0Budk0Fi/k2CDm2PEwa3BeXO5tPOA85b6xazE9rp8nF2MIJlsUik."
# user.save # => true
# user.authenticate('notright') # => false
# user.authenticate('mUc3m00RsqyRe') # => user
# user.authenticate_activation_token('a_new_token') # => user
# User.find_by(name: 'david').try(:authenticate, 'notright') # => false
# User.find_by(name: 'david').try(:authenticate, 'mUc3m00RsqyRe') # => user
def has_secure_password(options = {})
def has_secure_password(attribute = :password, validations: 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.
......@@ -63,9 +69,40 @@ def has_secure_password(options = {})
raise
end
include InstanceMethodsOnActivation
attr_reader attribute
define_method("#{attribute}=") do |unencrypted_password|
if unencrypted_password.nil?
self.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.send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost))
end
end
define_method("#{attribute}_confirmation=") do |unencrypted_password|
instance_variable_set("@#{attribute}_confirmation", unencrypted_password)
end
# Returns +self+ if the password is correct, otherwise +false+.
#
# class User < ActiveRecord::Base
# has_secure_password validations: false
# end
#
# user = User.new(name: 'david', password: 'mUc3m00RsqyRe')
# user.save
# user.authenticate_password('notright') # => false
# user.authenticate_password('mUc3m00RsqyRe') # => user
define_method("authenticate_#{attribute}") do |unencrypted_password|
attribute_digest = send("#{attribute}_digest")
BCrypt::Password.new(attribute_digest).is_password?(unencrypted_password) && self
end
alias_method :authenticate, :authenticate_password if attribute == :password
if options.fetch(:validations, true)
if validations
include ActiveModel::Validations
# This ensures the model has a password by checking whether the password_digest
......@@ -73,57 +110,13 @@ def has_secure_password(options = {})
# 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(:password, :blank) unless record.password_digest.present?
record.errors.add(attribute, :blank) unless record.send("#{attribute}_digest").present?
end
validates_length_of :password, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
validates_confirmation_of :password, allow_blank: true
validates_length_of attribute, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
validates_confirmation_of attribute, allow_blank: true
end
end
end
module InstanceMethodsOnActivation
# Returns +self+ if the password is correct, otherwise +false+.
#
# class User < ActiveRecord::Base
# has_secure_password validations: false
# end
#
# user = User.new(name: 'david', password: 'mUc3m00RsqyRe')
# user.save
# user.authenticate('notright') # => false
# user.authenticate('mUc3m00RsqyRe') # => user
def authenticate(unencrypted_password)
BCrypt::Password.new(password_digest).is_password?(unencrypted_password) && self
end
attr_reader :password
# Encrypts the password into the +password_digest+ attribute, only if the
# new password is not empty.
#
# class User < ActiveRecord::Base
# has_secure_password validations: false
# end
#
# user = User.new
# user.password = nil
# user.password_digest # => nil
# user.password = 'mUc3m00RsqyRe'
# user.password_digest # => "$2a$10$4LEA7r4YmNHtvlAvHhsYAeZmk/xeUVtMTYqwIvYY76EW5GUqDiP4."
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
def password_confirmation=(unencrypted_password)
@password_confirmation = unencrypted_password
end
end
end
end
......@@ -186,9 +186,13 @@ class SecurePasswordTest < ActiveModel::TestCase
test "authenticate" do
@user.password = "secret"
@user.activation_token = "new_token"
assert_not @user.authenticate("wrong")
assert @user.authenticate("secret")
assert !@user.authenticate_activation_token("wrong")
assert @user.authenticate_activation_token("new_token")
end
test "Password digest cost defaults to bcrypt default cost when min_cost is false" do
......
......@@ -7,6 +7,7 @@ class User
define_model_callbacks :create
has_secure_password
has_secure_password :activation_token, validations: false
attr_accessor :password_digest
attr_accessor :password_digest, :activation_token_digest
end
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册