隐藏ActiveRecord模型中的属性

时间:2012-11-19 21:59:13

标签: ruby-on-rails ruby activerecord

我对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 个答案:

答案 0 :(得分:1)

基于以上答案,我做了一些实验以获得所需的结果。我最终创建了一个“私有”password_hash属性和一个名为password的虚拟访问器。

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

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

  • 使用纯粹的东西覆盖password_hash访问器非常适合阻止对属性的操作,但是这也意味着在更新password_hash属性时ActiveRecord本身会因调用空实现而失败。

因此,使访问者私有化失败,因为它们在实际模型中未定义。定义它们也会失败,因为它会破坏ActiveRecord。那你能做什么?

我做了两件事,多做了一点。我将访问器设为私有,定义它们并通过调用super来实现它们。这可以防止控制器(和rails控制台)通过抛出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

答案 1 :(得分:0)

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

private :password, :password=

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

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

答案 2 :(得分:0)

我在寻找应用程序的身份验证解决方案时偶然发现了像设计这样的宝石,最后采用了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

答案 3 :(得分:0)

让我讲一个简短的故事,讲述为什么我必须像你一样经历类似的道路。我曾经不得不“取消订阅”我的模型的许多属性/列。

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

因此,不是通过方法(读取器和写入器)简单地重写方法,而是简单地编写method_missing以捕获所有旧列(通过正则表达式)并将其操作重定向到新逻辑。但是,如果我可以“取消定义”旧的访问器(“脏”)方法,我只能完成此操作,否则它们永远不会达到method_missing方法。

所以经过很多努力,我进入了以下解决方案(适应你的情况):

class Account < ActiveRecord::Base

  # ...

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

  # ...

end

我只需要删除读取器和写入器方法,但如果需要,可以删除所有其他“脏”方法。

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