ActiveModel::ForbiddenAttributesError

因为active_record不像pojo那样可通过getter/setter来获取自身需要的属性,而是给啥key/value它就拼出啥sql,所以,如果前端给了多余的field,则sql会出现不存在于schema中的column,为免从db报错,应尽早拦截此种错误

另一方面,即使column存在,你也不一定每种动作都为model赋那么多值,所以,params应能提供限制mass_assignment的功能

于是,便有了strong parameter

如果使用params直接给model赋值,最终是会raise ActiveModel::ForbiddenAttributesError的,调用栈如下



代码如下,如果是Parameters,则应permitted,如果不是,就无所谓,例如自己构造个hash来赋值

module ActiveModel

  class ForbiddenAttributesError < StandardError
  end

  module ForbiddenAttributesProtection # :nodoc:
    protected
      def sanitize_for_mass_assignment(attributes)
        if attributes.respond_to?(:permitted?)
          raise ActiveModel::ForbiddenAttributesError if !attributes.permitted?
          attributes.to_h
        else
          attributes
        end
      end
      alias :sanitize_forbidden_attributes :sanitize_for_mass_assignment
  end
end


require/permit

使用require/permit来限制mass_assignment,调用栈如下



require所做的就是,调用[],如果返回的是一个Hash,则convert_hashes_to_parameters,将此子集重新包装为一个Parameters,塞回到父集中,并返回这个包装后的子集

如果是返回的是普通字符串或已包装为Parameters,则原样返回

def convert_hashes_to_parameters(key, value)
  converted = convert_value_to_parameters(value)
  @parameters[key] = converted unless converted.equal?(value)
  converted
end

def convert_value_to_parameters(value)
  case value
  when Array
    return value if converted_arrays.member?(value)
    converted = value.map { |_| convert_value_to_parameters(_) }
    converted_arrays << converted
    converted
  when Hash
    self.class.new(value)
  else
    value
  end
end


另外,在调用[]的时候,是会检查该参数是否有值的,如果无,则会抛ParameterMissing。这避免了form嵌套参数或json嵌套,在传错某一嵌套层名称时,报undefine method `[]` for nil

def require(key)
 
  self[key].presence || raise(ParameterMissing.new(key))
 
end


permit所做的就是,抽取filters所指的key/value,建立一个新的Parameters,并递归设置它及其子集@permitted = true。

(子集也会因为在permitted_scalar_filter中用[]抽取,而convert_hashes_to_parameters。当然非Hash的子集就没有permit!方法了,所以if v.respond_to? :permit!)

def permit(*filters)
  params = self.class.new

  filters.flatten.each do |filter|
    case filter
    when Symbol, String
      permitted_scalar_filter(params, filter)
    when Hash then
      hash_filter(params, filter)
    end
  end

  unpermitted_parameters!(params) if self.class.action_on_unpermitted_parameters

  params.permit!
end

def permitted_scalar_filter(params, key)
  if has_key?(key) && permitted_scalar?(self[key])
    params[key] = self[key]
  end

  keys.grep(/\A#{Regexp.escape(key)}\(\d+[if]?\)\z/) do |k|
    if permitted_scalar?(self[k])
      params[k] = self[k]
    end
  end
end

def permit!
  each_pair do |key, value|
    Array.wrap(value).each do |v|
      v.permit! if v.respond_to? :permit!
    end
  end

  @permitted = true
  self
end


如果真的有多余的参数,而又想报警,可设置action_on_unpermitted_parameters来记录或抛异常

def unpermitted_parameters!(params)
  unpermitted_keys = unpermitted_keys(params)
  if unpermitted_keys.any?
    case self.class.action_on_unpermitted_parameters
    when :log
      name = "unpermitted_parameters.action_controller"
      ActiveSupport::Notifications.instrument(name, keys: unpermitted_keys)
    when :raise
      raise ActionController::UnpermittedParameters.new(unpermitted_keys)
    end
  end
end

def unpermitted_keys(params)
  self.keys - params.keys - self.always_permitted_parameters
end


为controller提供的params方法,直接就是使用了Parameters.new :

module StrongParameters
  extend ActiveSupport::Concern
  include ActiveSupport::Rescuable

  # Returns a new ActionController::Parameters object that
  # has been instantiated with the request.parameters.
  def params
    @_params ||= Parameters.new(request.parameters)
  end

  # Assigns the given +value+ to the +params+ hash. If +value+
  # is a Hash, this will create an ActionController::Parameters
  # object that has been instantiated with the given +value+ hash.
  def params=(value)
    @_params = value.is_a?(Hash) ? Parameters.new(value) : value
  end
end


然后在action_controller/base.rb中,为controller增加params方法:

MODULES = [
  #...
  StrongParameters,
  #...
]

MODULES.each do |mod|
  include mod
end


完整调用栈

strong_parameters.html

sanitize_for_mass_assignment.html