Rails 的安全机制
许多公司选择使用 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_protected
和 attr_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 提供以下选项:
- 禁用邮件发送:
- 将邮件保存在文件中:
- 拦截所有邮件并重定向到指定地址:
结语
强大的安全机制帮助开发者在使用 Rails 时构建可信、稳定且性能优化的应用程序。虽然有些机制可能显得繁琐,但它们远胜过因忽视安全而导致的严重问题。
关注公众号:程序新视界,一个让你软实力、硬技术同步提升的平台
除非注明,否则均为程序新视界原创文章,转载必须以链接形式标明本文链接