2

我对 Rails 有点陌生,正在使用 ActiveRecord 设计用户模型。在这个模型中,我有一个密码属性,用于保存用户密码的哈希值。

我想直接删除此属性的读取和设置。但是,在使用 Rails 控制台时,我似乎找不到删除访问器的方法。到目前为止,唯一可行的解​​决方案是显式覆盖密码的访问器方法,我真的不想覆盖它们,我希望访问器消失 - 或者至少是读者。

这是我的模型:

class User < ActiveRecord::Base

  // various associations

  def password_correct?(password)
    read_attribute(:password) == hash(password)
  end

  def password=(password)
    write_attribute(:password, hash(password))
  end

  def password
    "get your dirty fingers off this attribute"
  end

  private

  def hash(input)
    Digest::SHA2.new(512).update(input).hexdigest
  end

end

任何想法如何实现这一目标或这种方法的任何缺点?

4

4 回答 4

2

基于以上答案,我做了一些实验以获得预期的结果。我最终制作了一个“私有”密码哈希属性和一个名为密码的虚拟访问器。

在这个过程中我做了一些观察:

  • ActiveRecord 似乎没有任何私有属性的概念。使用诸如此类的符号将访问器方法设为私有private :password, :password=不是一种选择,因为 RailsNameError: undefined method在实例化模型时会抛出一个,因为模型本身没有定义这两个方法(它们似乎是从 继承的ActiveRecord::Base)。

  • 用纯无覆盖 password_hash 访问器对于防止对属性的操作非常有用,但这也意味着 ActiveRecord 本身在更新 password_hash 属性时会失败,因为它正在调用空实现。

因此,将访问器设为私有会失败,因为它们在实际模型中未定义。定义它们也失败了,因为它破坏了 ActiveRecord。所以,你可以做什么?

我做了两个,还有更多。我将访问器设为私有,定义它们并通过调用super. 这可以防止控制器(和 rails 控制台)通过抛出 a 来访问它们NoMethodError,但不会拒绝 ActiveRecord。

旁注:验证问题

我的方法遇到的一个问题是验证失败。对 password_hash 实施最小长度是不好的,因为任何密码(甚至没有)都会导致 128 个字符的 SHA512 哈希。所以验证哈希没有什么意义。相反,我向虚拟密码访问器添加了验证并添加了一个before_save :hash_password回调,该回调检查是否已设置虚拟访问器属性,如果已设置,则对其进行哈希处理并将其写入 password_hash 属性。

最终实施

我的实现以这种方式结束:

class User < ActiveRecord::Base
  attr_accessible :first_name, :last_name, :email
  attr_accessor :password
  validates :password, :length => { :minimum => 8 }, :if => :password_changed?
  validates :first_name, :last_name, :email, presence: true
  # Various associations
  before_save :hash_password

  def password_correct?(p)
    if(password.present?)
      password == p
    else
      read_attribute(:password_hash) == hash_string(p)
    end
  end

  def role_symbols
    roles.collect do |r|
      r.name.to_sym
    end
  end

  private

  def hash_string(input)
    Digest::SHA2.new(512).update(input).hexdigest
  end

  def hash_password
    if(password.present?)
      write_attribute(:password_hash, hash_string(password))
      self.password = nil
    end
  end

  def password_changed?
    password.present? or new_record?
  end

  def password_hash
    super
  end

  def password_hash=(p)
    super
  end

end
于 2012-11-22T22:29:54.773 回答
0

您可以使用私有方法轻松地将访问器设为私有,在您的模型中添加以下行:

private :password, :password=

另外,我建议您不要覆盖密码字段访问器。也许您可以命名password_hash数据库中的字段并将此列的访问器设为私有。然后按预期编写方法passwordpassword=

如果它不符合您的需求,您能解释一下为什么吗?为什么你想要的会是更好的解决方案?

于 2012-11-20T00:15:24.170 回答
0

我在为应用程序寻找身份验证解决方案时偶然发现了 devise 之类的宝石,最后采用了http://bcrypt-ruby.rubyforge.org/中举例说明的这种更简单的方法。

require 'bcrypt'

class Account < ActiveRecord::Base
  attr_accessor :password, :password_confirmation, :role

  def password
    @password ||= BCrypt::Password.new(crypted_password)
  end

  def password=(new_password)
    @password = BCrypt::Password.create(new_password)
    self.crypted_password = @password
  end
end

编辑:演示,在带有 ActivecRecord 和 Postgresql 的 Padrino 应用程序中实现

[6] pry(main)> a = Account.new
=> #<Account id: nil, name: nil, email: nil, role: nil, uid: nil, provider: nil, created_at: nil, updated_at: nil, crypted_password: nil>
[7] pry(main)> a.password = "asdf"
=> "asdf"
[8] pry(main)> a.password
=> "$2a$10$9udQKttf5zFqCv7da9ZY0uMsWYlbeGK3apEkIY6x05KND1v3vOkh2"
[9] pry(main)> a.password == "asdf"
=> true
于 2012-11-20T01:46:27.260 回答
0

让我讲一个简短的故事,说明为什么我必须走与您类似的道路。我曾经不得不“取消订阅”我模型的许多属性/列。

问题是我想通过read_attribute方法达到列值。我正在寻求几周后应该从数据库中删除这些列,所以我想首先优雅地迁移这些“已弃用”列中的数据(并将它们移植到新表中),但我仍然想使用整个应用程序中使用相同的旧属性访问器方法,但它们将使用新的数据库结构。

因此,我没有简单地逐个方法(读取器和写入器)覆盖方法,而是简单地编写method_missing以捕获所有旧列(通过正则表达式)并将它们的操作重定向到新逻辑中。但只有当我可以“取消定义”旧的访问器(“脏”)方法时,我才能做到这一点,否则它们将永远无法到达method_missing方法。

因此,经过一番挣扎后,我进入了以下解决方案(适应您的情况):

class Account < ActiveRecord::Base

  # ...

  unless attribute_methods_generated?
    define_attribute_methods
    undef_method "password"
    undef_method "password="
  end

  # ...

end

我只需要删除 reader 和 writter 方法,但如果需要,您可以删除所有其他“脏”方法。

attribute_methods_generated?如果脏方法已经生成,则类方法返回 true 。然后它强制生成脏方法,然后才删除所需的方法。如果您简单地尝试undef_method直接在类范围内使用方法,它会抛出一个异常,告诉您您尝试删除的方法不存在(尚)。这就是为什么你需要在上面那样做。

于 2014-04-17T20:36:49.613 回答