Hiding an attribute in an ActiveRecord model

后端 未结 4 1084
庸人自扰
庸人自扰 2021-01-21 19:06

I am somewhat new to Rails and working on designing a User model using ActiveRecord. In this model, I have a password attribute which is intended to keep a hash of the user\'s p

4条回答
  •  南方客
    南方客 (楼主)
    2021-01-21 19:44

    Based on the above answers, I have done some experimentation to obtain the desired result. I ended up making a "private" password_hash attribute and a virtual accessor called password.

    I made some observations in this process:

    • It seems that ActiveRecord doesn't have any concept of private attributes. Making the accessor methods private using symbols such as private :password, :password= is not an option, because Rails throws an NameError: undefined method when instantiating a model, as the model itself does not have these two methods defined (they seem to be inherited from ActiveRecord::Base).

    • Overriding the password_hash accessors with pure nothing is great for preventing the manipulation of the attribute, however it also means that ActiveRecord itself fails when updating the password_hash attribute as it is calling an empty implementation.

    So making the accessors private fails because they're undefined in the actual model. Defining them also fails, because it breaks ActiveRecord. So what can you do?

    I did both and a bit more. I made the accessors private, defined them and implemented both by calling super. This prevents the controller (and the rails console) from accessing them by throwing a NoMethodError, but doesn't reject ActiveRecord.

    A side note: Validation issues

    One problem I encountered with my approach was broken validation. Enforcing a minimum length on the password_hash was no good as any password (even nothing) results in a 128 character SHA512 hash. So validating the hash made little sense. Instead I added validation to the virtual password accessor and added a before_save :hash_password callback which checks to see if the virtual accessor attribute has been set and if so, hashes it and writes it to the password_hash attribute.

    Final implementation

    My implementation ended up this way:

    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
    

提交回复
热议问题