新手问题 has_secure_password 的问题

springwq · 2014年06月23日 · 最后由 billy 回复于 2014年06月24日 · 4791 次阅读

http://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html

使用 has_secure_password 的时候,应该要输入 password_confirmation 才可以 创建新用户成功的,为什么我只用了一个user.password = "bar" ,然后就生成新用户成功了呢?

.1.2 :001 > user = User.create(username: "foo")
   (0.1ms)  begin transaction
   (0.1ms)  rollback transaction
 => #<User id: nil, username: "foo", created_at: nil, updated_at: nil, password_digest: nil>
2.1.2 :002 > user.password = "bar"
 => "bar"
2.1.2 :003 > user.save
   (0.3ms)  begin transaction
Binary data inserted for `string` type on column `password_digest`
  SQL (0.5ms)  INSERT INTO "users" ("created_at", "password_digest", "updated_at", "username") VALUES (?, ?, ?, ?)  [["created_at", "2014-06-23 13:42:10.466978"], ["password_digest", "$2a$10$cecQPWxE3oyi78fU9FxFN.SQ1buu/jXY6gHF3tPwybRMQfNJg5tS6"], ["updated_at", "2014-06-23 13:42:10.466978"], ["username", "foo"]]
   (2.1ms)  commit transaction
 => true

Gemfile

 1 source 'https://rubygems.org'
 2
 3 gem 'rails', '4.1.1'
 4 gem 'sass-rails', '~> 4.0.0'
 5 gem 'uglifier', '>= 1.3.0'
 6 gem 'coffee-rails', '~> 4.0.0'
 7 gem 'bootstrap-sass', '~> 2.3.2.0'
 8 gem 'bcrypt'
 9 
10
11
12 gem 'jquery-rails'
13 gem 'turbolinks'
14 gem 'jbuilder', '~> 1.2'
15
16 group :doc do
17   gem 'sdoc', require: false
18 end
19
20 group :development do
21   gem 'quiet_assets'
22   gem 'pry'
23   gem 'sqlite3'
24   #gem  'mysql2'
25 end
26
27 group :production do
28   gem 'pg'
29   gem 'rails_12factor'
30 end

Model user.rb

1 class User < ActiveRecord::Base
2   has_many :posts
3   has_many :comments
4 
5   has_secure_password
6 end

@springwq 在你user = User.create(username: "foo", password: " ")时候如果没有加上 password_confirmation 那么这个默认是不生效的。如果按你的需求你应该改为user = User.create(username: "foo", password: "111 ", password_confirmation: "111") . If you don't need the confirmation validation, just don't set any value to the password_confirmation attribute and the validation will not be triggered. 附上原文解释http://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html

#1 楼 @yumu01 没仔细看文档,谢谢啦!这个和 rails 4.0 有点不同了。

楼主很细心啊。这个流程确实可以过,没问题,其实也不是 bug。道理是@yumu01提到的那句话,但实现起来是弯弯绕绕的。我来详细说一下。

主要的疑问应该在于,第一次的 create 和第二次的 save 都会触发 validation,但为什么第二次可以过呢?

首先我们需要明白 validation 是怎么实现的。主要原理是通过一个隐藏的 hash。

class_attribute :_validators
self._validators = Hash.new { |h,k| h[k] = [] }

https://github.com/rails/rails/blob/eddcdb0f1de6e7b1b503a6df8d60cd6a145ce080/activemodel/lib/active_model/validations.rb#L51

然后,第一次过 validate 的时候,如果这个 hash 的其中一个键值 attributes 是空 array, 那么每个在 model 里面定义过的 validates 都会被加进去:

https://github.com/rails/rails/blob/edb6f7db5495e6f683764e5bf5089cecab9cf7cd/activemodel/lib/active_model/validations/with.rb#L91

if validator.respond_to?(:attributes) && !validator.attributes.empty?
  validator.attributes.each do |attribute|
    _validators[attribute.to_sym] << validator
  end
else
  _validators[nil] << validator
end

validate(validator, options)

你也看到上面这段代码了,如果不是空的,那么后面的 validation 根本就不会添加任何新东西。

那好了,第一次create的时候有没有添加password_confirmation这个 attribute 呢?答案是否定的。原因在这:

if options.fetch(:validations, true)
  # 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(:password, :blank) unless record.password_digest.present?
  end

  validates_confirmation_of :password, if: ->{ password.present? }
end

https://github.com/rails/rails/blob/3bdf7b80a11dcb67b18553ff1fe0da82b0cffc20/activemodel/lib/active_model/secure_password.rb#L59

你看到了,如果 password 是空的,那么 password_confirmation 就不会有任何检查动作。

综合上面原理。你这个情况的流程是这样的

  1. User.create(name: 'foo')。这个 validation 肯定是失败的。但是,因为 attributes 里面没有 password,所以 password_confirmation 不会加到_validators[:attributes] 里面去。

  2. 第二次 save, validation 也触发。但是_validators[:attributes] 已经存在,所以程序不会再增加任何新的 attributes。而这个里面又没有 password_confirmation, 所以这一项不会检查。

不过在实际中,基本上不会有先 create,失败后再 save 的 use cases。所以这个并不是 bug。

看了下源码,在ConfirmationValidator中,validate_each的实现是这样的:

def validate_each(record, attribute, value)
  if (confirmed = record.send("#{attribute}_confirmation")) && (value != confirmed)
    human_attribute_name = record.class.human_attribute_name(attribute)
    record.errors.add(:"#{attribute}_confirmation", :confirmation, options.merge(attribute: human_attribute_name))
  end
end

注意,只有在password_confirmation有值的时候才会验证是否一致,否则就直接 pass 了。

#3 楼 @billy 非常感谢!解释的不能再详细了。这么看,这算是 rails 的 一个 feature 了。

@springwq 不客气:)不过不确定这能不能叫 feature, 貌似是基本功能吧。这个 has_secure_password 真是非常好用,简洁干净。

#3 楼 @billy 不对吧 按照这个说法 那如果我给password_confirmation赋不同的值而根本没有被检查 验证也可以过啦?

[Development]>> user = User.create username: 'bachue'
D, [2014-06-24T00:10:49.823699 #31046] DEBUG -- :    (0.1ms)  begin transaction
D, [2014-06-24T00:10:49.836657 #31046] DEBUG -- :    (0.1ms)  rollback transaction
=> #<User id: nil, username: "bachue", password_digest: nil, created_at: nil, updated_at: nil>
[Development]>> user.password = 'bachue'
=> "bachue"
[Development]>> user.valid?
=> true
[Development]>> user.password_confirmation = 'b'
=> "b"
[Development]>> user.valid?
=> false
[Development]>> user.save
D, [2014-06-24T00:11:07.133127 #31046] DEBUG -- :    (0.1ms)  begin transaction
D, [2014-06-24T00:11:07.134311 #31046] DEBUG -- :    (0.0ms)  rollback transaction
=> false

@iBachue 晚些我再研究一下。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号