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

我将这些方法设置为自动加密值。

class User < ApplicationRecord def name=(val) super val.encrypt end def name (super() || '').decrypt end 

当我尝试提交表单并且出现错误(缺少电话)时, name属性显示为乱码。

  

它在validation成功时起作用。 当我逐行通过我的控制器#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 ,并且仅在validation失败后才获取。


我将控制器减少到绝对最小值,并在#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 source

 Sue D. Nym 

我注意到attributes仍然返回加密值,所以我尝试添加这个,但form_for 仍然设法获取加密值并将其放在表单中!

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

Rails 5.0.2

虽然你可以通过重载name_before_type_case来解决这个name_before_type_case ,但我认为这实际上是进行这种转换的错误地方。

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

  1. 在记忆中的明文
  2. 在rest时加密

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

Rails 5引入了一个有用的Attributes 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属性将被透明地加密和解密。 这也意味着您可以将相同的转换应用于任意数量的属性,而无需重写相同的代码。

你为什么要把它作为名字露出来?

 class User < ApplicationRecord def decrypted_name=(val) name = val.encrypt end def decrypted_name name.decrypt end end 

然后使用@model.decrypted_name而不是@model.name因为名称已加密,并且这样保存在DB中。

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

如果加密的name不应该直接处理,而是使用这个decrypted_name访问器。

我发现了类似的问题: 输入字段方法(text_area,text_field等)如何从form_for块中的记录中获取属性值?

我补充道

  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