许多公司选择使用 Ruby on Rails 来构建产品,其原因在于 Rails 专注于效率。该框架提供了一系列默认设置,使开发者能专注于构建,而无需浪费时间进行大量配置。
然而,默认设置会有限制。如果开发者不够谨慎,可能会出现潜在问题。但不必担心!Rails 针对这些问题也有约定。本篇文章将探讨该框架提供的一些工具,帮助保护应用的安全性和性能。


Rails 的批量赋值保护

Rails 开发者学习的第一件事之一是如何处理表单参数。params 对象包含了所有表单参数,供我们在控制器和视图中使用。但当 Rails 生成 scaffold 时,它使用了一个名为“强参数”(Strong Parameters)的机制。

强参数是什么?

创建或更新一个 Active Record 模型时,可以传递一个可选的属性哈希:

User.create(name: "Jason Charnes", location: "Memphis")

这与 Rails 的表单助手工具完美结合:

<%= form_with model: @user do |form| %>
  <%= form.text_field :name %>
  <%= form.text_field :location %>
  <%= form.submit %>
<% end %>

表单向控制器发送的参数如下:

{user: {name: "Jason Charnes", location: "Memphis"}}

我们可以直接传递整个用户哈希:

class UsersController < ApplicationController
  def create
    User.create!(params[:user])
  end
end

警告 🚨

这种方式就是“批量赋值”,“批量赋值”是危险的。
虽然这种方法简单且易于使用,但是它可能会带来问题。例如,假设我们的 User 模型还有一个 admin 布尔字段,默认为 false。如果恶意用户往表单中添加如下隐藏字段:

<input type="hidden" name="user[admin]" value="true" />

参数会变为:

{user: {name: "Jason Charnes", location: "Memphis", admin: "true"}}

我们在创建记录时会将表单传递的所有参数,包括 admin 一并传递给 Active Record:

class UsersController < ApplicationController
  def create
    @user = User.create!(params[:user])
    @user.admin? #=> true
  end
end

这样,我们无意中赋予了用户管理员权限,后果可想而知。这正是强参数(Strong Parameters)的用武之地。

强参数解决方案

与直接传递原始参数不同,我们可以使用强参数来明确允许的属性:

class UsersController < ApplicationController
  def create
    User.create!(user_params)
  end

  private

  def user_params
    params.require(:user).permit(:name)
  end
end

这里我们定义了一个 user_params 方法,用于指定允许通过的特定参数。
该方法首先定义 params 对象中必须具有 :user 键的要求。如果缺少该键,将触发一个错误并阻止请求继续处理。接着,通过 permit 方法明确列出允许的属性。
现在,如果用户尝试传递 admin 参数,Rails 会将其从传递给 Active Record 的参数列表中排除。
Rails 默认会记录未被允许的参数。这既有助于在开发中调试,也有助于在生产环境中发现恶意行为者。如果想更进一步,可以配置 Rails 在发送未许可参数时抛出错误:

config.action_controller.action_on_unpermitted_parameters = :raise

虽然强参数是控制器层的功能,但过去 Rails 曾在模型层提供类似保护(如 attr_protectedattr_accessible)。然而,强参数模式更灵活;它允许根据上下文动态定义允许的属性。在某些情况下,管理员可以修改用户的 admin 状态,但普通用户不能。这种灵活性使在控制器中定义权限更加合适。


Rails 的 N+1 查询预防

Active Record 中,N+1 查询问题非常容易出现。虽然 Rails 会自动处理数据库查找,但可能会引发性能问题。
例如,我们渲染一个订单列表:

class OrdersController < ApplicationController
  def index
    @orders = Order.all
  end
end
<% @orders.each do |order| %>
  <tr>
    <td><%= order.id %></td>
    <td><%= order.customer.name %></td>
    <td><%= order.created_at %></td>
  </tr>
<% end %>

其中订单与客户是关联关系:

class Order < ApplicationRecord
  belongs_to :customer
end

这段代码会导致每次渲染订单时,查询客户数据。假设我们有 15 个订单,将会触发至少 15 次额外的客户查询。日志中显示:

SELECT "customers".* FROM "customers" WHERE "customers"."id" = $1 LIMIT $2

这就是 N+1 查询:N 代表订单记录数量,而 +1 表示每条记录的额外查询。

N+1 查询的解决方案

我们可以通过预加载关联关系来避免 N+1 查询:

class OrdersController < ApplicationController
  def index
    @orders = Order.includes(:customer).all
  end
end

.includes 方法会预加载关联关系,仅需一次额外查询即可加载所有客户。这显著减少了 SQL 查询次数。
Rails 6.1 引入了 strict_loading 机制,能在开发时检查是否发生懒加载并抛出 ActiveRecord::StrictLoadingViolationError 错误:

class Order < ApplicationRecord
  belongs_to :customer, strict_loading: true
end

在开发中启用全局严格加载可通过配置文件:

config.active_record.strict_loading_by_default = true

异步关联销毁

删除记录时通常会删除相关联的记录,可以使用如下设置:

class Order < ApplicationRecord
  has_many :invoices, dependent: :destroy_async
end

destroy_async 会将关联的记录销毁任务转移到后台任务队列,同时确保回调被正确执行。


响应失败的方式:显式失败与静默失败

Active Record 的方法可以抛出错误或保持静默。例如:

  • 显式失败:如果 create! 方法验证失败,会立即抛出错误并中止执行。
  • 静默失败:如果 create 方法验证失败,会返回一个无效的 Active Record 对象,而不会抛出错误。这种方式需要额外检查记录的有效性。

选择适当的失败响应方式能提升调试效率。


Rails 的凭据保护

Rails 提供内置机制用于安全存储凭据。开发者可以通过命令生成加密的凭据文件:

bin/rails credentials:edit --environment=development

凭据以加密 .yml.enc 文件保存,并通过唯一的密钥解密和访问。可这样访问:

Rails.application.credentials.stripe.secret_key

开发中的邮件安全

对于开发环境的邮件发送,Rails 提供以下选项:

  1. 禁用邮件发送:
    1. 将邮件保存在文件中:
      1. 拦截所有邮件并重定向到指定地址:

        结语

        强大的安全机制帮助开发者在使用 Rails 时构建可信、稳定且性能优化的应用程序。虽然有些机制可能显得繁琐,但它们远胜过因忽视安全而导致的严重问题。



        Rails 的安全机制插图

        关注公众号:程序新视界,一个让你软实力、硬技术同步提升的平台

        除非注明,否则均为程序新视界原创文章,转载必须以链接形式标明本文链接

        本文链接:http://www.choupangxia.com/2025/09/10/rails-safety/