Devise models run before_save multiple times?

2019-06-02 22:17发布

问题:

My client wants all user data encrypted, so I've created a before_save and after_find call back that will encrypt certain properties using Gibberish:

  # user.rb
  before_save UserEncryptor.new 
  after_find UserEncryptor.new

# user_encryptor.rb
class UserEncryptor
  def initialize
    @cipher = Gibberish::AES.new("password")
  end

  def before_save(user)
    user.first_name = encrypt(user.first_name)
    user.last_name = encrypt(user.last_name)
    user.email = encrypt(user.email) unless not user.confirmed? or user.unconfirmed_email
  end

  def after_find(user)
    user.first_name = decrypt(user.first_name)
    user.last_name = decrypt(user.last_name)
    user.email = decrypt(user.email) unless not user.confirmed? or user.unconfirmed_email
  end

  private
    def encrypt(value)
      @cipher.enc(value)
    end

    def decrypt(value)
      @cipher.dec(value)
    end
end

Well, when the user first signs up using Devise, the model looks about like it should. But then once the user confirms, if I inspect the user, the first_name and last_name properties look to have been encrypted multiple times. So I put a breakpoint in the before_save method and click the confirmation link, and I see that it's getting executed three times in a row. The result is that the encrypted value gets encrypted again, and then again, so next time we retrieve the record, and every time thereafter, we get a twice encrypted value.

Now, why the heck is this happening? It's not occurring for other non-devise models that are executing the same logic. Does Devise have the current_user cached in a few different places, and it saves the user in each location? How else could a before_save callback be called 3 times before the next before_find is executed?

And, more importantly, how can I successfully encrypt my user data when I'm using Devise? I've also had problems with attr_encrypted and devise_aes_encryptable so if I get a lot of those suggestions then I guess I have some more questions to post :-)

回答1:

I solved my problem with the help of a coworker.

For encrypting the first and last name, it was sufficient to add a flag to the model indicating whether or not it's been encrypted. That way, if multiple saves occur, the model knows it's already encrypted and can skip that step:

  def before_update(user)
    unless user.encrypted
      user.first_name = encrypt(user.first_name)
      user.last_name = encrypt(user.last_name)
      user.encrypted = true
    end
  end 

  def after_find(user) 
    if user.encrypted
      user.first_name = decrypt(user.first_name)
      user.last_name = decrypt(user.last_name)
      user.encrypted = false
    end 
  end

For the email address, this was not sufficient. Devise was doing some really weird stuff with resetting cached values, so the email address was still getting double encrypted. So instead of hooking into the callbacks to encrypt the email address, we overrode some methods on the user model:

  def email_before_type_cast
    super.present? ? AES.decrypt(super, KEY) : ""
  end 

  def email
    return "" unless self[:email]
    @email ||= AES.decrypt(self[:email], KEY)
  end

  def email=(provided_email)
    self[:email] = encrypted_email(provided_email)
    @email = provided_email
  end

  def self.find_for_authentication(conditions={})
    conditions[:email] = encrypted_email(conditions[:email])
    super
  end

  def self.find_or_initialize_with_errors(required_attributes, attributes, error=:invalid)
    attributes[:email] = encrypted_email(attributes[:email]) if attributes[:email]
    super
  end

  def self.encrypted_email decrypted_email
    AES.encrypt(decrypted_email, KEY, {:iv => IV})
  end

This got us most of the way there. However, my Devise models are reconfirmable, so when I changed a user's email address and tried to save, the reconfirmable module encountered something funky, the record got saved like a hundred times or so, and then I got a stack overflow and a rollback. We found that we needed to override one more method on the user model to do the trick:

  def email_was
    super.present? ? AES.decrypt(super, KEY) : ""
  end

Now all of our personally identifiable information is encrypted! Yay!