Hanami 的 Action 在处理 HTTP 请求时,会把查询字符串、表单字段和 JSON 请求体统一封装成 Hanami::Action::Parameters 对象,挂载在 request.params 上。这个对象不是普通的 Ruby Hash,它带有一组用于参数过滤的接口,其中最重要的就是 require 和 permit。如果开发者直接把 params 整体传给业务层或 ORM,攻击者可能传入 id、admin、created_at 等敏感字段,造成批量赋值安全问题。下面通过具体代码演示如何建立强参数白名单。

理解 require 与 permit 的职责
require 方法负责保证某个根键必须存在。例如创建用户的请求体应该包含 user 键,里面才是 name 和 email。如果请求缺失 user 键,require(:user) 会立即抛出参数异常,Action 不会继续执行后续逻辑。这个设计能尽早暴露客户端错误,避免用 nil 去读取内部字段。要求根键后,拿到的返回值仍然是一个 Parameters 对象,因此可以继续链式调用 permit。
permit 方法作用是白名单过滤,只有列出的字段会被保留,其他任何额外字段都会被丢弃。假设客户端在 user 对象里传入了 role 或 admin 字段,permit(:name, :email) 之后它们不会出现在结果中。返回对象的类型依然是 Hanami::Action::Parameters,但内部只包含许可字段。可以调用 to_h 转成普通 Hash 或者直接用索引访问。
module MyApp
module Actions
class CreateUser
include Hanami::Action
def handle(req, res)
user = req.params
.require(:user)
.permit(:name, :email, :age)
# user 现在只包含 name、email、age 三个字段
res.body = user.to_h.to_json
end
end
end
end
如果请求体是 {"user":{"name":"Alice","email":"alice@ipipp.com","role":"admin"}},最终 user.to_h 将只包含 name 和 email,age 没传也不会报错。permit 不会校验字段是否存在或是否非空,它只做包含过滤。因此如果需要必填校验,应配合 Hanami 的验证器或自定义逻辑。require 和 permit 的组合体现了白名单思想:先明确外部形状,再控制内部字段,不可信任的部分不会进入业务代码。
嵌套参数白名单的声明方式
实际业务中的用户模型经常带有嵌套属性,比如 profile 哈希、addresses 数组。Hanami::Action::Parameters 的 permit 方法支持嵌套白名单,语法与 Rails strong parameters 类似。例如要允许 user 下的 name 以及 profile 内的 bio 和 avatar,可以写成 permit(:name, profile: [:bio, :avatar])。这样既保留了 name,又只保留 profile 中的两个字段,profile 里其他键会被丢弃。
class UpdateUser
include Hanami::Action
def handle(req, res)
data = req.params
.require(:user)
.permit(:name, profile: [:bio, :avatar, :location])
# 过滤后 profile 内部只保留 bio、avatar、location 三个键
res.body = data.to_h.to_json
end
end
当嵌套结构是数组时,声明方式有所区别。比如用户可以有多个收货地址,每个地址对象包含 street 和 city。此时需要写成 permit(:name, addresses: [[:street, :city]])。外层数组的写法表示 addresses 是一个数组,数组中的每个元素都是哈希,且每个哈希只允许 street 和 city。如果写成 addresses: [:street, :city] 则是完全不同的含义,它表示 addresses 键本身是一个哈希,而不是数组。这是嵌套白名单中最容易混淆的地方。
class CreateOrder
include Hanami::Action
def handle(req, res)
data = req.params
.require(:order)
.permit(:user_id, addresses: [[:street, :city, :zip]])
# 过滤后 addresses 保留为数组,每个元素是仅含 street/city/zip 的哈希
res.body = data.to_h.to_json
end
end
更深层的结构,比如 profile 里再嵌 settings 哈希,可用多层哈希:permit(:name, profile: [:bio, settings: [:theme, :locale]])。这种声明读起来从外向内,字段越多嵌套越深。对于动态键或不确定深度的结构,白名单机制会显得笨重,这时建议把数据拆分成更扁平的参数结构,或使用专门的 schema 校验库。关键原则是:嵌套白名单要尽量反映真实的业务数据结构,且不要为了省事使用 permit 全部放行,那样会失去安全意义。
过滤后的对象行为与异常处理实践
经过 require 和 permit 处理后,返回的仍然是 Hanami::Action::Parameters 对象,而不是普通 Hash。它支持索引访问、迭代、to_h 等方法,但只有调用 to_h 或 to_hash 后才会变成纯 Ruby Hash。需要注意,Parameters 对象可能保留原始输入的键类型,JSON 解析通常是字符串键。如果业务代码习惯用符号键访问,可以在过滤后统一转换,例如借助 transform_keys 方法转换成符号键,或者直接使用字符串键。
def handle(req, res)
begin
data = req.params
.require(:user)
.permit(:name, :email, profile: [:bio])
# 转换为符号键的 Hash
user_hash = data.to_h.transform_keys(&:to_sym)
user = UserRepository.new.create(user_hash)
res.status = 201
res.body = user.to_json
rescue Hanami::Action::Parameters::Error => e
res.status = 422
res.body = { error: e.message }.to_json
end
end
上面代码中,begin rescue 捕获了 require 失败时抛出的异常。实际开发中,不必在每个 Action 里重复书写 rescue 逻辑,可以提取一个基类或模块统一处理参数异常。Hanami 的 Action 支持 before 回调或错误处理器,你可以定义一个类似 rescue_from 风格的方法,让所有参数异常都返回统一的 JSON 错误格式。这样业务代码只需关注成功路径,错误处理保持集中。
需要提醒的是,强参数过滤只解决字段白名单问题,并不做类型转换和格式验证。例如年龄传入字符串 "18" 时 permit 不会自动转换成整数,email 格式错误也不会被拦截。因此完整的参数处理流程通常是:先 require + permit 过滤,再使用 dry-validation 或 Hanami::Validator 进行类型和规则校验,最后把安全的数据传入业务层。过滤与校验分离,既能防止批量赋值,又能保证数据质量。
Hanami::Action::Parameters 通过 require 和 permit 两个方法实现了强参数白名单机制,嵌套哈希与数组的声明方式清晰但需要耐心理解。工程中建议将其作为第一道防线,并结合验证器完成后续检查。这样即使面对复杂 JSON 请求体,也能保持代码安全和可维护。
Hanami::Action::Parameters强参数嵌套参数白名单修改时间:2026-09-18 18:18:31