更多内容请访问 rubyonrails.org:

Action Controller 概述

在本指南中,你将了解控制器的工作原理以及它们如何适应应用程序中的请求周期。

阅读本指南后,你将了解如何

  • 跟随请求通过控制器流转。
  • 访问传递给控制器的参数。
  • 使用 Strong Parameters 并允许值。
  • 将数据存储在 cookie、session 和 flash 中。
  • 使用动作回调在请求处理期间执行代码。
  • 使用请求和响应对象。

1. 简介

Action Controller 是模型-视图-控制器 (MVC) 模式中的 C。在路由器将传入请求匹配到控制器后,控制器负责处理请求并生成相应的输出。

对于大多数传统的 RESTful 应用程序,控制器将接收请求,从模型中获取或保存数据,并使用视图创建 HTML 输出。

你可以想象控制器位于模型和视图之间。控制器使模型数据可供视图使用,以便视图可以将该数据显示给用户。控制器还从视图接收用户输入并相应地保存或更新模型数据。

2. 创建控制器

控制器是继承自 ApplicationController 的 Ruby 类,并且像任何其他类一样具有方法。一旦传入请求被路由器匹配到控制器,Rails 就会创建该控制器类的一个实例,并调用与动作同名的方法。

class ClientsController < ApplicationController
  def new
  end
end

给定上述 ClientsController,如果用户在应用程序中访问 /clients/new 以添加新客户端,Rails 将创建 ClientsController 的一个实例并调用其 new 方法。如果 new 方法为空,Rails 默认会自动渲染 new.html.erb 视图。

这里的 new 方法是实例方法,在 ClientsController 的实例上调用。这不应与 new 类方法(即 ClientsController.new)混淆。

new 方法中,控制器通常会创建 Client 模型的一个实例,并将其作为名为 @client 的实例变量提供给视图。

def new
  @client = Client.new
end

所有控制器都继承自 ApplicationController,而 ApplicationController 又继承自 ActionController::Base。对于仅 API 的应用程序,ApplicationController 继承自 ActionController::API

2.1. 控制器命名约定

Rails 倾向于将控制器名称中的资源设为复数。例如,ClientsController 优于 ClientControllerSiteAdminsController 优于 SiteAdminControllerSitesAdminsController。但是,复数名称并非严格要求(例如 ApplicationController)。

遵循此命名约定将允许你使用默认路由生成器(例如 resources),而无需为每个生成器指定诸如 :controller 之类的选项。该约定还使命名路由助手在整个应用程序中保持一致。

控制器命名约定与模型不同。虽然控制器名称倾向于使用复数,但模型名称倾向于使用单数形式(例如 Account vs. Accounts)。

控制器动作应为 public,因为只有 public 方法才能作为动作调用。降低不打算作为动作的辅助方法(使用 privateprotected)的可见性也是最佳实践。

Action Controller 保留了一些方法名称。意外地重新定义它们可能导致 SystemStackError。如果你将控制器仅限于 RESTful 资源路由动作,则无需担心这个问题。

如果你必须使用保留方法作为动作名称,一种解决方法是使用自定义路由将保留方法名称映射到你的非保留动作方法。

3. 参数

传入请求发送的数据在控制器的 params 哈希中可用。有两种类型的参数数据:

  • 查询字符串参数,它们作为 URL 的一部分发送(例如,在 http://example.com/accounts?filter=free 中的 ? 之后)。
  • 从 HTML 表单提交的 POST 参数。

Rails 不区分查询字符串参数和 POST 参数;两者都在控制器的 params 哈希中可用。例如:

class ClientsController < ApplicationController
  # This action receives query string parameters from an HTTP GET request
  # at the URL "/clients?status=activated"
  def index
    if params[:status] == "activated"
      @clients = Client.activated
    else
      @clients = Client.inactivated
    end
  end

  # This action receives parameters from a POST request to "/clients" URL with  form data in the request body.
  def create
    @client = Client.new(params[:client])
    if @client.save
      redirect_to @client
    else
      render "new"
    end
  end
end

params 哈希不是普通的 Ruby 哈希;相反,它是一个 ActionController::Parameters 对象。它不继承自哈希,但其行为大部分像哈希。它提供了过滤 params 的方法,而且与哈希不同,键 :foo"foo" 被认为是相同的。

3.1. 哈希和数组参数

params 哈希不限于一维键和值。它可以包含嵌套数组和哈希。要发送一个值数组,请在键名后附加一对空方括号 []

GET /users?ids[]=1&ids[]=2&ids[]=3

此示例中的实际 URL 将被编码为 /users?ids%5b%5d=1&ids%5b%5d=2&ids%5b%5d=3,因为 [] 字符不允许在 URL 中。大多数情况下你无需担心这一点,因为浏览器会为你编码,Rails 会自动解码,但如果你需要手动向服务器发送这些请求,则应牢记这一点。

params[:ids] 的值将是数组 ["1", "2", "3"]。请注意,参数值始终是字符串;Rails 不会尝试猜测或转换类型。

出于安全原因,params 中的值(如 [nil][nil, nil, ...])默认替换为 []。有关详细信息,请参阅安全指南

要发送一个哈希,你将键名包含在方括号内:

<form accept-charset="UTF-8" action="/users" method="post">
  <input type="text" name="user[name]" value="Acme" />
  <input type="text" name="user[phone]" value="12345" />
  <input type="text" name="user[address][postcode]" value="12345" />
  <input type="text" name="user[address][city]" value="Carrot City" />
</form>

提交此表单后,params[:user] 的值将是 { "name" => "Acme", "phone" => "12345", "address" => { "postcode" => "12345", "city" => "Carrot City" } }。请注意 params[:user][:address] 中的嵌套哈希。

params 对象表现得像一个哈希,但允许你将符号和字符串作为键互换使用。

3.2. 复合键参数

复合键参数在一个参数中包含多个值,并用分隔符(例如下划线)分隔。因此,你需要提取每个值,以便可以将它们传递给 Active Record。你可以使用 extract_value 方法来完成此操作。

例如,给定以下控制器:

class BooksController < ApplicationController
  def show
    # Extract the composite ID value from URL parameters.
    id = params.extract_value(:id)
    @book = Book.find(id)
  end
end

以及此路由:

get "/books/:id", to: "books#show"

当用户请求 URL /books/4_2 时,控制器将提取复合键值 ["4", "2"] 并将其传递给 Book.findextract_value 方法可用于从任何带分隔符的参数中提取数组。

3.3. JSON 参数

如果你的应用程序公开了 API,你很可能会接受 JSON 格式的参数。如果请求的 content-type 标头设置为 application/json,Rails 会自动将你的参数加载到 params 哈希中,你可以像往常一样访问它。

所以,例如,如果你正在发送以下 JSON 内容:

{ "user": { "name": "acme", "address": "123 Carrot Street" } }

你的控制器将收到:

{ "user" => { "name" => "acme", "address" => "123 Carrot Street" } }

3.3.1. 配置 Wrap Parameters

你可以使用 Wrap Parameters,它会自动将控制器名称添加到 JSON 参数中。例如,你可以发送以下 JSON,而无需根 :user 键前缀:

{ "name": "acme", "address": "123 Carrot Street" }

假设你将上述数据发送到 UsersController,JSON 将被包装在 :user 键中,如下所示:

{ name: "acme", address: "123 Carrot Street", user: { name: "acme", address: "123 Carrot Street" } }

Wrap Parameters 将参数的克隆添加到哈希中,其键与控制器名称相同。因此,参数的原始版本和“包装”版本都将存在于 params 哈希中。

此功能会克隆参数并使用根据控制器名称选择的键进行包装。它默认配置为 true。如果你不想包装参数,可以将其配置false

config.action_controller.wrap_parameters_by_default = false

你还可以自定义要包装的键或特定参数的名称,更多信息请参阅 API 文档

3.4. 路由参数

routes.rb 文件中作为路由声明的一部分指定的参数也在 params 哈希中可用。例如,我们可以添加一个路由,捕获客户端的 :status 参数:

get "/clients/:status", to: "clients#index", foo: "bar"

当用户导航到 /clients/active URL 时,params[:status] 将被设置为 "active"。当使用此路由时,params[:foo] 也将被设置为 "bar",就像它在查询字符串中传递一样。

路由声明定义的任何其他参数,例如 :id,也将可用。

在上述示例中,你的控制器还将收到 params[:action] 为 "index" 和 params[:controller] 为 "clients"。params 哈希将始终包含 :controller:action 键,但建议使用方法 controller_nameaction_name 来访问这些值。

3.5. default_url_options 方法

你可以通过在控制器中定义名为 default_url_options 的方法来为 url_for 设置全局默认参数。例如:

class ApplicationController < ActionController::Base
  def default_url_options
    { locale: I18n.locale }
  end
end

指定的默认值将作为生成 URL 的起点。它们可以通过传递给 url_for 或任何路径助手(例如 posts_path)的选项来覆盖。例如,通过设置 locale: I18n.locale,Rails 将自动为每个 URL 添加语言环境:

posts_path # => "/posts?locale=en"

如果需要,你仍然可以覆盖此默认值:

posts_path(locale: :fr) # => "/posts?locale=fr"

在底层,posts_path 是调用 url_for 并带有适当参数的简写。

如果你在 ApplicationController 中定义 default_url_options,如上例所示,这些默认值将用于所有 URL 生成。该方法也可以在特定控制器中定义,在这种情况下,它仅适用于为该控制器生成的 URL。

在给定请求中,该方法实际上不会为每个生成的 URL 调用。出于性能原因,返回的哈希会按请求进行缓存。

4. Strong Parameters

使用 Action Controller Strong Parameters,参数在明确允许之前不能用于 Active Model 的批量赋值。这意味着你需要决定允许哪些属性进行批量更新并在控制器中声明它们。这是一种安全实践,以防止用户意外更新敏感模型属性。

此外,参数可以标记为必填,如果未传递所有必填参数,请求将返回 400 Bad Request。

class PeopleController < ActionController::Base
  # This will raise an ActiveModel::ForbiddenAttributesError
  # because it's using mass assignment without an explicit permit.
  def create
    Person.create(params[:person])
  end

  # This will work as we are using `person_params` helper method, which has the
  # call to `expect` to allow mass assignment.
  def update
    person = Person.find(params[:id])
    person.update!(person_params)
    redirect_to person
  end

  private
    # Using a private method to encapsulate the permitted parameters is a good
    # pattern. You can use the same list for both create and update.
    def person_params
      params.expect(person: [:name, :age])
    end
end

4.1. 允许值

4.1.1. expect

expect 方法提供了一种简洁安全的方式来要求和允许参数。

id = params.expect(:id)

上述 expect 总是返回一个标量值,而不是数组或哈希。另一个例子是表单参数,你可以使用 expect 来确保根键存在并且属性是允许的。

user_params = params.expect(user: [:username, :password])
user_params.has_key?(:username) # => true

在上面的示例中,如果 :user 键不是具有指定键的嵌套哈希,expect 将引发错误并返回 400 Bad Request 响应。

要要求和允许整个参数哈希,可以使用 expect 如下所示。

params.expect(log_entry: {})

这会将 :log_entry 参数哈希及其任何子哈希标记为允许,并且不检查允许的标量,任何内容都被接受。

调用 expect 时应极其小心地传递空哈希,因为它将允许所有当前和未来的模型属性进行批量赋值。

4.1.2. permit

调用 permit 允许 params 中指定的键(下面的 :id:admin)包含在批量赋值中(例如通过 createupdate)。

params = ActionController::Parameters.new(id: 1, admin: "true")
=> #<ActionController::Parameters {"id"=>1, "admin"=>"true"} permitted: false>
params.permit(:id)
=> #<ActionController::Parameters {"id"=>1} permitted: true>
params.permit(:id, :admin)
=> #<ActionController::Parameters {"id"=>1, "admin"=>"true"} permitted: true>

对于允许的键 :id,其值还需要是以下允许的标量值之一:StringSymbolNilClassNumericTrueClassFalseClassDateTimeDateTimeStringIOIOActionDispatch::Http::UploadedFileRack::Test::UploadedFile

如果你未对键调用 permit,它将被过滤掉。数组、哈希或任何其他对象默认不会被注入。

要在 params 中包含一个由允许的标量值组成的数组,你可以将键映射到一个空数组,如下所示:

params = ActionController::Parameters.new(tags: ["rails", "parameters"])
=> #<ActionController::Parameters {"tags"=>["rails", "parameters"]} permitted: false>
params.permit(tags: [])
=> #<ActionController::Parameters {"tags"=>["rails", "parameters"]} permitted: true>

要包含哈希值,可以映射到一个空哈希:

params = ActionController::Parameters.new(options: { darkmode: true })
=> #<ActionController::Parameters {"options"=>{"darkmode"=>true}} permitted: false>
params.permit(options: {})
=> #<ActionController::Parameters {"options"=>#<ActionController::Parameters {"darkmode"=>true} permitted: true>} permitted: true>

上面的 permit 调用确保 options 中的值是允许的标量,并过滤掉其他任何内容。

带有空哈希的 permit 很方便,因为有时不可能或不方便声明哈希参数的每个有效键或其内部结构。但请注意,上面带有空哈希的 permit 会打开任意输入的漏洞。

4.1.3. permit!

还有 permit!(带 !),它允许整个参数哈希而不检查值。

params = ActionController::Parameters.new(id: 1, admin: "true")
=> #<ActionController::Parameters {"id"=>1, "admin"=>"true"} permitted: false>
params.permit!
=> #<ActionController::Parameters {"id"=>1, "admin"=>"true"} permitted: true>

使用 permit! 时应极其小心,因为它将允许所有当前和未来的模型属性进行批量赋值。

4.2. 嵌套参数

你还可以在嵌套参数上使用 expect(或 permit),例如:

# Given the example expected params:
params = ActionController::Parameters.new(
  name: "Martin",
  emails: ["me@example.com"],
  friends: [
    { name: "André", family: { name: "RubyGems" }, hobbies: ["keyboards", "card games"] },
    { name: "Kewe", family: { name: "Baroness" }, hobbies: ["video games"] },
  ]
)
# the following expect will ensure the params are permitted
name, emails, friends = params.expect(
  :name,                 # permitted scalar
  emails: [],            # array of permitted scalars
  friends: [[            # array of permitted Parameter hashes
    :name,               # permitted scalar
    family: [:name],     # family: { name: "permitted scalar" }
    hobbies: []          # array of permitted scalars
  ]]
)

此声明允许 nameemailsfriends 属性并分别返回它们。预期 emails 将是允许的标量值数组,并且 friends 将是具有特定属性的资源数组(注意新的双数组语法以明确要求数组)。这些属性应具有 name 属性(允许任何允许的标量值),hobbies 属性作为允许的标量值数组,以及 family 属性,该属性限制为仅具有 name 键和任何允许的标量值的哈希。

4.3. 示例

以下是针对不同用例如何使用 permit 的一些示例。

示例 1:你可能希望在 new 动作中使用允许的属性。这引发了一个问题,即你不能对根键使用 require,因为通常在调用 new 时它不存在:

# using `fetch` you can supply a default and use
# the Strong Parameters API from there.
params.fetch(:blog, {}).permit(:title, :author)

示例 2:模型类方法 accepts_nested_attributes_for 允许你更新和销毁关联记录。这是基于 id_destroy 参数的:

# permit :id and :_destroy
params.expect(author: [ :name, books_attributes: [[ :title, :id, :_destroy ]] ])

示例 3:带有整数键的哈希处理方式不同,你可以像它们是直接子级一样声明属性。当你将 accepts_nested_attributes_forhas_many 关联结合使用时,会得到这类参数:

# To permit the following data:
# {"book" => {"title" => "Some Book",
#             "chapters_attributes" => { "1" => {"title" => "First Chapter"},
#                                        "2" => {"title" => "Second Chapter"}}}}

params.expect(book: [ :title, chapters_attributes: [[ :title ]] ])

示例 4:设想一个场景,你有一些参数表示产品名称,以及与该产品关联的任意数据哈希,并且你希望允许产品名称属性以及整个数据哈希:

def product_params
  params.expect(product: [ :name, data: {} ])
end

5. Cookies

Cookie 的概念并非 Rails 特有。 Cookie(也称为 HTTP cookie 或网络 cookie)是服务器发送的一小段数据,保存在用户浏览器中。浏览器可以存储 cookie、创建新 cookie、修改现有 cookie,并在后续请求中将其发送回服务器。Cookie 可以在 Web 请求之间持久化数据,从而使 Web 应用程序能够记住用户偏好。

Rails 通过 cookies 方法提供了一种简单的方式来访问 cookie,其工作方式类似于哈希:

class CommentsController < ApplicationController
  def new
    # Auto-fill the commenter's name if it has been stored in a cookie
    @comment = Comment.new(author: cookies[:commenter_name])
  end

  def create
    @comment = Comment.new(comment_params)
    if @comment.save
      if params[:remember_name]
        # Save the commenter's name in a cookie.
        cookies[:commenter_name] = @comment.author
      else
        # Delete cookie for the commenter's name, if any.
        cookies.delete(:commenter_name)
      end
      redirect_to @comment.article
    else
      render action: "new"
    end
  end
end

要删除 cookie,你需要使用 cookies.delete(:key)。将 key 设置为 nil 值不会删除 cookie。

当传递标量值时,cookie 将在用户关闭浏览器时删除。如果你希望 cookie 在特定时间过期,请在设置 cookie 时传递带有 :expires 选项的哈希。例如,设置一个 1 小时后过期的 cookie:

cookies[:login] = { value: "XJ-122", expires: 1.hour }

如果你想创建永不过期的 cookie,请使用永久 cookie 存储。这将使分配的 cookie 的过期日期设置为从现在起 20 年。

cookies.permanent[:locale] = "fr"

5.1. 加密和签名 Cookies

由于 Cookie 存储在客户端浏览器上,它们可能容易受到篡改,因此不被认为是存储敏感数据的安全方式。Rails 提供了签名 Cookie 罐和加密 Cookie 罐来存储敏感数据。签名 Cookie 罐在 Cookie 值上附加了加密签名,以保护其完整性。加密 Cookie 罐除了签名外还会加密值,这样用户就无法读取它们。

有关更多详细信息,请参阅 API 文档

class CookiesController < ApplicationController
  def set_cookie
    cookies.signed[:user_id] = current_user.id
    cookies.encrypted[:expiration_date] = Date.tomorrow # => Thu, 20 Mar 2024
    redirect_to action: "read_cookie"
  end

  def read_cookie
    cookies.encrypted[:expiration_date] # => "2024-03-20"
  end
end

这些特殊的 cookie 存储使用序列化器将 cookie 值序列化为字符串,并在读取时将它们反序列化为 Ruby 对象。你可以通过 config.action_dispatch.cookies_serializer 指定要使用的序列化器。新应用程序的默认序列化器是 :json

请注意,JSON 对序列化 Ruby 对象(如 DateTimeSymbol)的支持有限。这些对象将被序列化和反序列化为 String

如果你需要存储这些或更复杂的对象,你可能需要在后续请求中读取它们时手动转换其值。

如果你使用 cookie 会话存储,上述内容也适用于 sessionflash 哈希。

6. 会话 (Session)

虽然 cookie 存储在客户端,但会话数据存储在服务器端(内存、数据库或缓存中),并且持续时间通常是临时的,并与用户会话绑定(例如,直到他们关闭浏览器)。会话的一个用例是存储敏感数据,如用户身份验证。

在 Rails 应用程序中,会话在控制器和视图中都可用。

6.1. 处理会话

你可以使用 session 实例方法在控制器中访问会话。会话值以键/值对的形式存储,类似于哈希:

class ApplicationController < ActionController::Base
  private
    # Look up the key `:current_user_id` in the session and use it to
    # find the current `User`. This is a common way to handle user login in
    # a Rails application; logging in sets the session value and
    # logging out removes it.
    def current_user
      @current_user ||= User.find_by(id: session[:current_user_id]) if session[:current_user_id]
    end
end

要将会话中存储某些内容,你可以将其分配给一个键,类似于向哈希添加值。用户通过身份验证后,其 id 将保存在会话中,以供后续请求使用:

class SessionsController < ApplicationController
  def create
    if user = User.authenticate_by(email: params[:email], password: params[:password])
      # Save the user ID in the session so it can be used in
      # subsequent requests
      session[:current_user_id] = user.id
      redirect_to root_url
    end
  end
end

要从会话中删除某些内容,请删除键/值对。从会话中删除 current_user_id 键是注销用户的典型方法:

class SessionsController < ApplicationController
  def destroy
    session.delete(:current_user_id)
    # Clear the current user as well.
    @current_user = nil
    redirect_to root_url, status: :see_other
  end
end

可以使用 reset_session 重置整个会话。建议在登录后使用 reset_session 以避免会话固定攻击。详细信息请参阅安全指南

会话是延迟加载的。如果你在动作代码中不访问会话,它们将不会被加载。因此,你永远不需要禁用会话——不访问它们即可完成任务。

6.2. 闪存 (Flash)

闪存 (flash) 提供了一种在控制器动作之间传递临时数据的方法。你放入闪存中的任何内容都将在下一个动作中可用,然后被清除。闪存通常用于在控制器动作中设置消息(例如通知和警报),然后在重定向到向用户显示消息的动作之前使用。

通过 flash 方法访问闪存。与会话类似,闪存值以键/值对的形式存储。

例如,在控制器用于注销用户的动作中,控制器可以设置一个闪存消息,该消息可以在下一个请求中显示给用户:

class SessionsController < ApplicationController
  def destroy
    session.delete(:current_user_id)
    flash[:notice] = "You have successfully logged out."
    redirect_to root_url, status: :see_other
  end
end

在用户在应用程序中执行某些交互操作后显示消息是一种良好的实践,可以向用户反馈他们的操作是否成功(或是否存在错误)。

除了 :notice,你还可以使用 :alert。这些通常使用不同的颜色(使用 CSS)进行样式化,以指示它们的含义(例如,绿色表示通知,橙色/红色表示警报)。

你还可以通过在 redirect_to 方法中将其作为参数包含来直接在 redirect_to 方法中分配闪存消息:

redirect_to root_url, notice: "You have successfully logged out."
redirect_to root_url, alert: "There was an issue."

你不仅限于 noticealert。你可以通过将其分配给 :flash 参数来在闪存中设置任何键(类似于会话)。例如,分配 :just_signed_up

redirect_to root_url, flash: { just_signed_up: true }

这将允许你在视图中拥有以下内容:

<% if flash[:just_signed_up] %>
  <p class="welcome">Welcome to our site!</p>
<% end %>

在上面的注销示例中,destroy 动作重定向到应用程序的 root_url,在那里消息可以显示。但是,它不会自动显示。由下一个动作决定如何处理(如果处理)上一个动作放入闪存中的内容。

6.2.1. 显示闪存消息

如果之前的动作*设置了*闪存消息,最好将其显示给用户。我们可以通过在应用程序的默认布局中添加显示任何闪存消息的 HTML 来实现一致性。这是 app/views/layouts/application.html.erb 中的一个示例:

<html>
  <!-- <head/> -->
  <body>
    <% flash.each do |name, msg| -%>
      <%= content_tag :div, msg, class: name %>
    <% end -%>

    <!-- more content -->
    <%= yield %>
  </body>
</html>

上面的 name 表示闪存消息的类型,例如 noticealert。此信息通常用于设置消息显示给用户的样式。

如果你想限制布局中只显示 noticealert,你可以按 name 过滤。否则,flash 中设置的所有键都将显示。

在布局中包含闪存消息的读取和显示可确保你的应用程序自动显示这些消息,而无需每个视图都包含读取闪存的逻辑。

6.2.2. flash.keepflash.now

flash.keep 用于将闪存值延续到另一个请求。这在有多个重定向时很有用。

例如,假设控制器中的 index 动作对应于 root_url。并且你希望这里的所有请求都重定向到 UsersController#index。如果一个动作设置了闪存并重定向到 MainController#index,这些闪存值将在另一个重定向发生时丢失,除非你使用 flash.keep 使值在另一个请求中持久化。

class MainController < ApplicationController
  def index
    # Will persist all flash values.
    flash.keep

    # You can also use a key to keep only some kind of value.
    # flash.keep(:notice)
    redirect_to users_url
  end
end

flash.now 用于使闪存值在同一请求中可用。默认情况下,将值添加到闪存会使其在下一个请求中可用。例如,如果 create 动作无法保存资源,并且你直接渲染 new 模板,那不会导致新请求,但你可能仍希望使用闪存显示消息。为此,你可以像使用普通 flash 一样使用 flash.now

class ClientsController < ApplicationController
  def create
    @client = Client.new(client_params)
    if @client.save
      # ...
    else
      flash.now[:error] = "Could not save client"
      render action: "new"
    end
  end
end

6.3. 会话存储

所有会话都有一个代表会话对象的唯一 ID;这些会话 ID 存储在 cookie 中。实际的会话对象使用以下存储机制之一:

对于大多数会话存储,cookie 中的唯一会话 ID 用于在服务器上查找会话数据(例如数据库表)。Rails 不允许你在 URL 中传递会话 ID,因为这不太安全。

6.3.1. CookieStore

CookieStore 是默认且推荐的会话存储。它将所有会话数据存储在 cookie 本身中(如果需要,会话 ID 仍然可用)。CookieStore 轻量级,在新应用程序中使用无需任何配置。

CookieStore 可以存储 4 KB 数据——远少于其他存储选项——但这通常足够了。不鼓励在会话中存储大量数据。你应特别避免在会话中存储复杂对象(例如模型实例)。

6.3.2. CacheStore

如果你的会话不存储关键数据或不需要长时间存在(例如,你只是使用闪存进行消息传递),则可以使用 CacheStore。这将使用你为应用程序配置的缓存实现来存储会话。优点是你可以使用现有的缓存基础设施来存储会话,而无需任何额外的设置或管理。缺点是会话存储将是临时的,并且它们可能随时消失。

有关会话存储的更多信息,请阅读安全指南

6.4. 会话存储选项

有一些与会话存储相关的配置选项。你可以在初始化文件中配置存储类型:

Rails.application.config.session_store :cache_store

Rails 在签名会话数据时设置会话键(cookie 的名称)。这些也可以在初始化文件中更改:

Rails.application.config.session_store :cookie_store, key: "_your_app_session"

修改初始化文件后务必重启服务器。

你还可以传递 :domain 键并为 cookie 指定域名:

Rails.application.config.session_store :cookie_store, key: "_your_app_session", domain: ".example.com"

有关更多信息,请参阅配置指南中的 config.session_store

Rails 在 config/credentials.yml.enc 中为 CookieStore 设置了一个用于签署会话数据的密钥。凭证可以通过 bin/rails credentials:edit 更新。

# aws:
#   access_key_id: 123
#   secret_access_key: 345

# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
secret_key_base: 492f...

在使用 CookieStore 时更改 secret_key_base 将使所有现有会话失效。你需要配置一个 cookie 旋转器来旋转现有会话。

7. 控制器回调

控制器回调是自动在控制器动作之前和/或之后运行的方法。控制器回调方法可以在给定控制器或 ApplicationController 中定义。由于所有控制器都继承自 ApplicationController,因此在此处定义的回调将在应用程序中的每个控制器上运行。

7.1. before_action

通过 before_action 注册的回调方法在控制器动作之前运行。它们可以中止请求周期。before_action 的一个常见用例是确保用户已登录:

class ApplicationController < ActionController::Base
  before_action :require_login

  private
    def require_login
      unless logged_in?
        flash[:error] = "You must be logged in to access this section"
        redirect_to new_login_url # halts request cycle
      end
    end
end

如果用户未登录,该方法会将错误消息存储在闪存中并重定向到登录表单。当 before_action 回调渲染或重定向时(如上例所示),原始控制器动作不会运行。如果注册了额外的回调要运行,它们也会被取消并且不运行。

在此示例中,before_actionApplicationController 中定义,因此应用程序中的所有控制器都继承它。这意味着应用程序中的所有请求都将要求用户登录。这对于“登录”页面来说没问题。但是,“登录”动作即使在用户未登录时也应该成功(以允许用户登录),否则用户将永远无法登录。你可以使用 skip_before_action 允许指定的控制器动作跳过给定的 before_action

class LoginsController < ApplicationController
  skip_before_action :require_login, only: [:new, :create]
end

现在,LoginsControllernewcreate 动作将无需用户登录即可工作。

:only 选项仅跳过列出的动作的回调;还有一个 :except 选项,其作用相反。这些选项在注册动作回调时也可以使用,以添加仅对选定动作运行的回调。

如果你使用不同的选项多次注册相同的动作回调,则最后一个动作回调定义将覆盖之前的定义。

7.2. after_actionaround_action

你还可以定义动作回调,使其在控制器动作执行之后运行,使用 after_action,或者在之前和之后都运行,使用 around_action

after_action 回调与 before_action 回调类似,但由于控制器动作已经运行,它们可以访问即将发送给客户端的响应数据。

after_action 回调仅在控制器动作成功执行后才执行,如果请求周期中抛出异常则不会执行。

around_action 回调在需要在控制器动作之前和之后执行代码时非常有用,允许你封装影响动作执行的功能。它们通过 yield 来负责运行其关联的动作。

例如,假设你想监控特定动作的性能。你可以使用 around_action 来测量每个动作完成所需的时间并记录此信息:

class ApplicationController < ActionController::Base
  around_action :measure_execution_time

  private
    def measure_execution_time
      start_time = Time.now
      yield  # This executes the action
      end_time = Time.now

      duration = end_time - start_time
      Rails.logger.info "Action #{action_name} from controller #{controller_name} took #{duration.round(2)} seconds to execute."
    end
end

动作回调接收 controller_nameaction_name 作为参数,你可以使用它们,如上例所示。

around_action 回调也包装了渲染。在上面的示例中,视图渲染将包含在 duration 中。around_actionyield 之后的代码即使在关联动作中出现异常并且回调中有 ensure 块时也会运行。(这与 after_action 回调不同,其中动作中的异常会取消 after_action 代码。)

7.3. 使用回调的其他方式

除了 before_actionafter_actionaround_action,还有两种不太常见的方式来注册回调。

第一种是直接将一个块与 *_action 方法一起使用。该块将控制器作为参数接收。例如,上面的 require_login 动作回调可以重写为使用一个块:

class ApplicationController < ActionController::Base
  before_action do |controller|
    unless controller.send(:logged_in?)
      flash[:error] = "You must be logged in to access this section"
      redirect_to new_login_url
    end
  end
end

请注意,在这种情况下,动作回调使用 send,因为 logged_in? 方法是私有的,并且动作回调不在控制器的作用域中运行。这不是实现此特定动作回调的推荐方式,但在更简单的情况下可能很有用。

特别是对于 around_action,该块也会在 action 中 yield:

around_action { |_controller, action| time(&action) }

第二种方法是为回调动作指定一个类(或任何响应预期方法的对象)。这在更复杂的情况下很有用。例如,你可以使用一个类重写 around_action 回调来测量执行时间:

class ApplicationController < ActionController::Base
  around_action ActionDurationCallback
end

class ActionDurationCallback
  def self.around(controller)
    start_time = Time.now
    yield # This executes the action
    end_time = Time.now

    duration = end_time - start_time
    Rails.logger.info "Action #{controller.action_name} from controller #{controller.controller_name} took #{duration.round(2)} seconds to execute."
  end
end

在上面的示例中,ActionDurationCallback 的方法不在控制器作用域中运行,但将 controller 作为参数获取。

通常,用于 *_action 回调的类必须实现一个与动作回调同名的方法。因此,对于 before_action 回调,该类必须实现一个 before 方法,依此类推。此外,around 方法必须 yield 以执行动作。

8. 请求和响应对象

每个控制器都有两个方法,requestresponse,可用于访问与当前请求周期关联的请求和响应对象。request 方法返回一个 ActionDispatch::Request 实例。 response 方法返回一个 ActionDispatch::Response 实例,该对象表示将返回给客户端浏览器的数据(例如控制器动作中的 renderredirect)。

8.1. request 对象

请求对象包含来自客户端的请求的有用信息。本节描述 request 对象的一些属性的用途。

要获取可用方法的完整列表,请参阅 Rails API 文档Rack 文档

request 的属性 用途
host 此请求使用的主机名。
domain(n=2) 主机名从右侧(TLD)开始的第一个 n 段。
format(格式) 客户端请求的内容类型。
method 用于请求的 HTTP 方法。
get?, post?, patch?, put?, delete?, head? 如果 HTTP 方法是 GET/POST/PATCH/PUT/DELETE/HEAD,则返回 true。
headers 返回一个哈希,包含与请求关联的标头。
port 用于请求的端口号(整数)。
protocol 返回一个字符串,包含使用的协议以及 "://",例如 "http://"。
query_string URL 的查询字符串部分,即 "?" 之后的所有内容。
remote_ip 客户端的 IP 地址。
url 用于请求的完整 URL。

8.1.1. query_parametersrequest_parameterspath_parameters

Rails 将给定请求的所有参数收集到 params 哈希中,包括在 URL 中设置为查询字符串参数的参数,以及作为 POST 请求体发送的参数。请求对象有三个方法,可以让你访问各种参数。

8.1.2. request.variant

控制器可能需要根据请求中的上下文特定信息来调整响应。例如,响应来自移动平台的请求的控制器可能需要渲染与来自桌面浏览器的请求不同的内容。实现这一目标的一种策略是自定义请求的变体。变体名称是任意的,可以传达请求的平台(:android, :ios, :linux, :macos, :windows)到其浏览器(:chrome, :edge, :firefox, :safari),再到用户的类型(:admin, :guest, :user)。

你可以在 before_action 中设置 request.variant

request.variant = :tablet if request.user_agent.include?("iPad")

在控制器动作中以变体响应就像以格式响应一样:

# app/controllers/projects_controller.rb

def show
  # ...
  respond_to do |format|
    format.html do |html|
      html.tablet                         # renders app/views/projects/show.html+tablet.erb
      html.phone { extra_setup; render }  # renders app/views/projects/show.html+phone.erb
    end
  end
end

应为每种格式和变体创建单独的模板:

  • app/views/projects/show.html.erb
  • app/views/projects/show.html+tablet.erb
  • app/views/projects/show.html+phone.erb

你也可以使用内联语法简化变体定义:

respond_to do |format|
  format.html.tablet
  format.html.phone  { extra_setup; render }
end

8.2. response 对象

响应对象在动作执行期间,通过渲染将发送给客户端浏览器的数据而构建。通常不直接使用它,但有时,例如在 after_action 回调中,直接访问响应可能很有用。一个用例是设置内容类型头:

response.content_type = "application/pdf"

另一个用例是设置自定义响应头:

response.headers["X-Custom-Header"] = "some value"

headers 属性是一个哈希,它将头名称映射到头值。Rails 会自动设置一些头,但如果你需要更新头或添加自定义头,可以使用 response.headers,如上例所示。

headers 方法也可以直接在控制器中访问。

以下是 response 对象的一些属性:

response 的属性 用途
body 这是发送回客户端的数据字符串。这通常是 HTML。
status 响应的 HTTP 状态码,例如成功请求的 200 或文件未找到的 404。
location 客户端被重定向到的 URL(如果有)。
content_type 响应的内容类型。
charset 用于响应的字符集。默认为 "utf-8"。
headers 用于响应的标头。

要获取可用方法的完整列表,请参阅 Rails API 文档Rack 文档



回到顶部