`form_for`绕过了模型访问器。如何停止它?(或者:如何创建自定义属性序列化程序?)

7
我设置了这些方法来自动加密值。
class User < ApplicationRecord
  def name=(val)
    super val.encrypt
  end
  def name
    (super() || '').decrypt
  end

当我试图提交表单并出现错误(缺少电话号码),则name属性显示为乱码。
<input class="form-control" type="text" value="Mg8IS1LB2A1efAeZJxIDJMSroKcq6WueyY4ZiUX+hfI=" name="user[name]" id="user_name">

只有在验证成功时它才能正常工作。当我通过控制器#update逐行执行代码时,它也可以在控制台中正常工作。

irb(main):015:0> u = User.find 1
irb(main):016:0> u.name
=> "Sue D. Nym"
irb(main):017:0> u.phone
=> "212-555-1234"
irb(main):018:0> u.update name: 'Sue D. Nym', phone: ''
   (10.0ms)  BEGIN
   (1.0ms)  ROLLBACK
=> false
irb(main):020:0> u.save
=> false
irb(main):029:0> u.errors.full_messages.join ','
=> "Phone can't be blank"
irb(main):031:0> u.build_image unless u.image
=> nil
irb(main):033:0> u.name
=> "Sue D. Nym"
users_controller.rb
  def update
    @user = User.find current_user.id
    @user.update user_params
    if @user.save
      flash.notice = "Profile Saved"
      redirect_to :dashboard
    else
      flash.now.alert = @user.errors.full_messages.join ', '
      @user.build_image unless @user.image
      render :edit
    end
  end

视图在经过验证失败后,以某种方式获取了加密值,而没有经过#name


我将控制器最小化,但在#update之后立即失败。 然而,在控制台上它是有效的!

  def update
    @user = User.find current_user.id
    @user.update user_params
    render :edit
    return

我将视图缩小到最小,显示了名称,但仅在form_for之外。我还不知道原因。

edit.haml
=@user.name
=form_for @user, html: { multipart: true } do |f|
  =f.text_field :name

HTML源代码
<span>Sue D. Nym</span>
<form class="edit_user" id="edit_user_1" enctype="multipart/form-data" action="/users/1" accept-charset="UTF-8" method="post">
  <input name="utf8" type="hidden" value="✓"><input type="hidden" name="_method" value="patch"><input type="hidden" name="authenticity_token" value="C/ScTxfENNxCKgzG0qAlPElOKI7nOYxZimQ7BsB64wIWQ9El4+vOAfxX3qHL08rbr0sxRiJnzQti13e4DAgkfQ==">  
  <input type="text" value="sER9cjwa6Ov5weXjEQN2KJYoTOXtVBytpX/cI/aPrFs=" name="user[name]" id="user_name">
</form>

我注意到attributes仍然返回加密的值,所以我尝试添加了这个代码,但是form_for仍然能够获取加密的值并将其放入表单中!

  def attributes
    attr_hash = super()
    attr_hash["name"] = name
    attr_hash
  end

Rails 5.0.2


这是哪个版本的Rails? - Adam Lassek
根据该帖子,它是Rails 5.0.2版本。 - Robin
看一下attr_encrypted。虽然它基本上做的是同样的事情,但它更加“经得起考验”。在这里没有必要自己开发解决方案。 - Sergio Tulentsev
这需要为每个表字段单独设置一个IV字段,需要将每个字段重命名为“encrypted_field”,它与下面的答案类似,只是它切换到使用“encrypted_name”作为真实字段和虚拟的“name”方法而不是“decrypted_name”。 - Chloe
@Robin 这是在我的问题之后添加的。 - Adam Lassek
3个回答

6

虽然可以通过重载name_before_type_case来解决此问题,但我认为这实际上不是应该进行此类转换的正确位置。

根据您的示例,此处的要求似乎为:

  1. 内存中为纯文本
  2. 在静止状态下加密

因此,如果我们将加密/解密转换移动到Ruby-DB边界,此逻辑将变得更清晰和可重用。

Rails 5引入了一个有用的属性API,用于处理此场景。由于您没有提供有关如何实现加密程序的详细信息,因此我将在示例代码中使用Base64来演示文本转换。

app/types/encrypted_type.rb
class EncryptedType < ActiveRecord::Type::Text
  # this is called when saving to the DB
  def serialize(value)
    Base64.encode64(value) unless value.nil?
  end

  # called when loading from DB
  def deserialize(value)
    Base64.decode64(value) unless value.nil?
  end

  # add this if the field is not idempotent
  def changed_in_place?(raw_old_value, new_value)
    deserialize(raw_old_value) != new_value
  end
end
config/initalizers/types.rb
ActiveRecord::Type.register(:encrypted, EncryptedType)

现在,您可以在模型中将此属性指定为加密的:
class User < ApplicationRecord
  attribute :name, :encrypted

  # If you have a lot of fields, you can use metaprogramming:
  %i[name phone address1 address2 ssn].each do |field_name|
    attribute field_name, :encrypted
  end
end

name 属性在往返到数据库时将被自动加密和解密。这也意味着您可以对尽可能多的属性应用相同的转换,而无需重写相同的代码。


有没有一种方式可以一次列出多个属性,例如 attribute :name, :phone, :address1, :encryptedcast value 是什么意思? - Chloe
每次调用User.save时,这会更新数据库,因为它生成一个新值和一个新的IV,而getter/setter/getter_before_type_cast则不会。> u.save (3.0ms) BEGIN User Exists (2.0ms) ... SQL (2.0ms) UPDATE "users" SET "name" = $1, ... #serialize仅包含value.encrypt unless value.nil? - Chloe
是的,它为每个值生成一个随机IV。但由于某种原因,此实现在加载后(或调用changed?)立即调用serialize来生成新的加密值。因此,仅加载模型就需要保存数十个具有新值的字段,即使没有任何更改。这可能会减慢数据库速度。 - Chloe
1
@Chloe 尝试重载 changed_in_place? 以考虑你的加密。 - Adam Lassek
@Chloe,你不应该以那种方式使用类变量,因为它们与继承结合使用时会产生一些不良副作用。 - Adam Lassek
显示剩余4条评论

1
为什么你要将它暴露成名称呢?
class User < ApplicationRecord
    def decrypted_name=(val)
       name = val.encrypt
    end

    def decrypted_name
       name.decrypt
    end
end

然后您使用@model.decrypted_name代替@model.name,因为名称已加密,并在数据库中保存。

edit.haml
=@user.decrypted_name
=form_for @user, html: { multipart: true } do |f|
  =f.text_field :decrypted_name

如果名称已加密,则不应直接处理,而应使用此decrypted_name访问器。

这与他已经拥有的根本不同在哪里?为什么不从视图的角度透明地使用“name”? - Sergio Tulentsev
那我将不得不在代码的各处将所有属性更改为decrypted_[attribute],而且有1-2打属性我想要加密,而且我希望它能够透明地工作。 - Chloe

1
我发现了一个类似的问题:如何在form_for块中从记录获取属性值以使用input字段方法(text_area,text_field等)? 我添加了


  def name_before_type_cast
    (super() || '').decrypt
  end

现在它可以工作了!

以下是完整的解决方案:

  @@encrypted_fields = [:name, :phone, :address1, :address2, :ssn, ...]
  @@encrypted_fields.each do |m|
    setter = (m.to_s+'=').to_sym
    getter = m
    getter_btc = (m.to_s+'_before_type_cast').to_sym
    define_method(setter) do |v|
      super v.encrypt
    end
    define_method(getter) do
      (super() || '').decrypt
    end
    define_method(getter_btc) do
      (super() || '').decrypt
    end
  end

一些文档:http://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/BeforeTypeCast.html



网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接