`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
,但我认为这实际上是进行这种转换的错误地方。
根据您的示例,此处的要求似乎是:
- 在记忆中的明文
- 在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