v7.0.0
更多資訊請前往 rubyonrails.org: 更多在 Ruby on Rails

Action Controller 概述

在本指南中,您將瞭解 controllers 如何工作以及它們如何適應您的應用程式中的請求週期。

閱讀本指南後,您將瞭解:

1 Controller 有什麼作用?

Action Controller 是 MVC 中的 C。在路由器確定用於請求的 controller 後,controller 負責理解請求併產生適當的輸出。幸運的是,Action Controller 為您完成了大部分基礎工作,並使用智慧約定使這儘可能簡單。

對於大多數傳統的 RESTful 應用程式,controller 將接收請求(作為開發人員的您是不可見的),從 model 獲取或儲存資料,並使用一個 view 來建立 HTML 輸出。如果你的 controller 需要做一些不同的事情,那不是問題,這只是 controller 最常見的工作方式。

因此,可以將 controller 視為 models 和 views 之間的中間人。它使模型資料可用於檢視,因此它可以向用戶顯示該資料,並將使用者資料儲存或更新到模型中。

有關路由過程的更多詳細資訊,請參閱 Rails Routing from the Outside In

2 Controller 命名約定

Rails 中 controllers 的命名約定有利於控制器名稱中最後一個單詞的複數形式,儘管它不是嚴格要求的(例如 ApplicationController)。例如,ClientsController 優先於 ClientControllerSiteAdminsController 優先於 SiteAdminControllerSitesAdminsController,依此類推。

遵循此約定將允許您使用預設路由生成器(例如 resources 等),而無需限定每個 :path:controller,並將在整個應用程式中保持命名路由 helpers 的使用一致。有關詳細資訊,請參閱 佈局和渲染指南

controller 命名約定與 models 命名約定不同,後者應以單數形式命名。

3 方法和 Actions

controller 是一個 Ruby 類,它繼承自 ApplicationController 並具有與任何其他類一樣的方法。當您的應用程式收到請求時,路由將確定要執行哪個 controller 和 action,然後 Rails 建立該 controller 的實例並執行與 action 同名的方法。

class ClientsController < ApplicationController
  def new
  end
end

例如,如果使用者在您的應用程式中轉到 /clients/new 以新增新客戶端,Rails 將建立 ClientsController 的實例並呼叫其 new 方法。請注意,上面示例中的空方法可以正常工作,因為除非 action 另有說明,否則 Rails 將預設呈現 new.html.erb view。透過建立新的 Clientnew 方法可以使 @client 實例變數在 view 中可訪問:

def new
  @client = Client.new
end

佈局和渲染指南 更詳細地解釋了這一點。

ApplicationController 繼承自 [ActionController][],它定義了許多有用的方法。本指南將介紹其中的一些內容,但如果您想了解其中的內容,可以在 API 文件 中檢視所有內容或在源頭本身。

只有公共方法可以作為 actions 呼叫。最佳做法是降低不打算成為 actions 的方法(使用 privateprotected)的可見性,例如輔助方法或過濾器。

警告:某些方法名稱由 Action Controller 保留。不小心將它們重新定義為 actions,甚至作為輔助方法,可能會導致 SystemStackError。如果您將 controllers 限制為僅 RESTful 資源路由 actions,則無需擔心這一點。

如果您必須使用保留方法作為 action 名稱,一種解決方法是使用自定義路由將保留方法名稱對映到非保留 action 方法。

4 引數

您可能希望訪問使用者傳送的資料或 controller actions 中的其他引數。 Web 應用程式中有兩種可能的引數。第一個是作為 URL 的一部分發送的引數,稱為查詢字串引數。查詢字串是“?”之後的所有內容在網址中。第二種型別的引數通常稱為 POST 資料。此資訊通常來自使用者填寫的 HTML 表單。之所以稱為 POST 資料,是因為它只能作為 HTTP POST 請求的一部分發送。 Rails 不區分查詢字串引數和 POST 引數,兩者都可以在 controller 的 params 雜湊中使用:

class ClientsController < ApplicationController
  # This action uses query string parameters because it gets run
  # by an HTTP GET request, but this does not make any difference
  # to how the parameters are accessed. The URL for
  # this action would look like this to list activated
  # clients: /clients?status=activated
  def index
    if params[:status] == "activated"
      @clients = Client.activated
    else
      @clients = Client.inactivated
    end
  end

  # This action uses POST parameters. They are most likely coming
  # from an HTML form that the user has submitted. The URL for
  # this RESTful request will be "/clients", and the data will be
  # sent as part of the request body.
  def create
    @client = Client.new(params[:client])
    if @client.save
      redirect_to @client
    else
      # This line overrides the default rendering behavior, which
      # would have been to render the "create" view.
      render "new"
    end
  end
end

4.1 雜湊和陣列引數

params 雜湊值不限於一維 keys 和 values。它可以包含巢狀陣列和雜湊。要傳送一個 values 陣列,請將一對空的方括號“[]”附加到鍵名:

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

本示例中的實際 URL 將被編碼為“/clients?ids%5b%5d=1&ids%5b%5d=2&ids%5b%5d=3”,因為“[”和“]”字元在網址。大多數時候你不必擔心這個,因為瀏覽器會為你編碼,Rails 會自動解碼,但是如果你發現自己必須手動將這些請求傳送到伺服器,你應該記住這一點.

params[:ids] 的值現在將為 ["1", "2", "3"]。請注意,引數 values 始終是字串; Rails 不會嘗試猜測或強制轉換型別。

替換 params 中的 Value,例如 [nil][nil, nil, ...] 出於安全原因,預設使用 []。參見安全指南 想要查詢更多的資訊。

要傳送雜湊,請在括號內包含 key 名稱:

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

提交此表單時,params[:client] 的 value 將為 { "name" => "Acme", "phone" => "12345", "address" => { "postcode" => "12345", "city" => "Carrot City" } }。注意 params[:client][:address] 中的巢狀雜湊。

params 物件的作用類似於雜湊,但允許您使用 symbols 和字串作為 keys 交替使用。

4.2 JSON 引數

如果您正在編寫 Web 服務應用程式,您可能會發現自己更願意接受 JSON 格式的引數。如果您的請求的“Content-Type”標頭設定為“application/json”,Rails 會自動將您的引數載入到 params 雜湊中,您可以像往常一樣訪問它。

例如,如果您要傳送此 JSON 內容:

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

您的 controller 將收到 params[:company] 作為 { "name" => "acme", "address" => "123 Carrot Street" }

此外,如果您在初始化程式中打開了 config.wrap_parameters 或在 controller 中呼叫了 wrap_parameters,則可以安全地省略 JSON 引數中的根元素。在這種情況下,引數將被克隆並使用根據您的 controller 名稱選擇的 key 包裝。所以上面的JSON請求可以寫成:

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

並且,假設您將資料傳送到 CompaniesController,那麼它將像這樣包裝在 :company key 中:

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

您可以透過查閱API文件自定義key的名稱或要包裝的特定引數

對解析 XML 引數的支援已被提取到名為 actionpack-xml_parser 的 gem 中。

4.3 路由引數

params 雜湊將始終包含 :controller:action keys,但您應該使用方法 controller_nameaction_name 來訪問這些 values。路由定義的任何其他引數,例如 :id,也將可用。例如,考慮一個客戶端列表,該列表可以顯示活動或非活動客戶端。我們可以在“漂亮”的 URL 中新增一個捕獲 :status 引數的路由:

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

在這種情況下,當用戶開啟 URL /clients/active 時,params[:status] 將設定為“活動”。使用此路由時,params[:foo] 也將設定為“bar”,就像在查詢字串中傳遞一樣。您的 controller 還將收到 params[:action] 作為“索引”和 params[:controller] 作為“客戶”。

4.4 default_url_options

您可以透過在 controller 中定義一個名為 default_url_options 的方法來設定 URL 生成的全域性預設引數。這樣的方法必須返回具有所需預設值的雜湊值,其 keys 必須是 symbols:

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

這些選項將在生成 URL 時用作起點,因此它們可能會被傳遞給 url_for 呼叫的選項覆蓋。

如果您在 ApplicationController 中定義 default_url_options,如上例所示,這些預設值將用於所有 URL 生成。該方法也可以在特定的 controller 中定義,在這種情況下,它只會影響在那裡生成的 URL。

在給定的請求中,實際上並不是為每個生成的 URL 呼叫該方法。出於效能原因,返回的雜湊被快取,並且每個請求最多有一次呼叫。

4.5 強引數

帶強引數,禁止Action Controller引數 在 Active Model 質量分配中使用,直到它們被 允許。這意味著你必須做出有意識的決定 哪些屬性允許批次更新。這是一個更好的安全 有助於防止意外允許使用者更新敏感資訊的做法 model 屬性。

此外,引數可以根據需要進行標記,並會流經一個 將導致 400 Bad Request 的預定義提升/救援流程 如果沒有傳入所有必需的引數,則返回。

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

  # This will pass with flying colors as long as there's a person key
  # in the parameters, otherwise it'll raise an
  # ActionController::ParameterMissing exception, which will get
  # caught by ActionController::Base and turned into a 400 Bad
  # Request error.
  def update
    person = current_account.people.find(params[:id])
    person.update!(person_params)
    redirect_to person
  end

  private
    # Using a private method to encapsulate the permissible parameters
    # is just a good pattern since you'll be able to reuse the same
    # permit list between create and update. Also, you can specialize
    # this method with per-user checking of permissible attributes.
    def person_params
      params.require(:person).permit(:name, :age)
    end
end
4.5.1 允許的標量 Values

像這樣呼叫 permit:

params.permit(:id)

如果指定的 key (:id) 出現在 params 和 它有一個允許的標量 value 關聯。否則,key 會去 被過濾掉,所以陣列、雜湊或任何其他物件不能被 注入。

允許的標量型別為 StringSymbolNilClassNumeric, TrueClass, FalseClass, Date, Time, DateTime, StringIOIOActionDispatch::Http::UploadedFileRack::Test::UploadedFile

宣告 params 中的 value 必須是一個允許的陣列 標量 values,將 key 對映到一個空陣列:

params.permit(id: [])

有時宣告有效的 keys 是不可能或不方便的 雜湊引數或其內部結構。只需對映到空雜湊:

params.permit(preferences: {})

但要小心,因為這為任意輸入打開了大門。在這 在這種情況下,permit 確保返回結構中的 values 是允許的 標量並過濾掉其他任何東西。

要允許引數的完整雜湊,可以使用 permit! 方法 用過的:

params.require(:log_entry).permit!

這將 :log_entry 引數雜湊及其任何子雜湊標記為 允許並且不檢查允許的標量,任何東西都被接受。 使用 permit! 時應格外小心,因為它將允許所有電流 和未來的 model 屬性將被批次分配。

4.5.2 巢狀引數

您還可以對巢狀引數使用 permit,例如:

params.permit(:name, { emails: [] },
              friends: [ :name,
                         { family: [ :name ], hobbies: [] }])

此宣告允許 nameemailsfriends 屬性。預計 emails 將是一個允許的陣列 標量 values,而 friends 將是一個資源陣列 特定屬性:他們應該有一個 name 屬性(任何 允許標量 values 允許),一個 hobbies 屬性作為一個數組 允許的標量 values 和受限制的 family 屬性 有一個 name(這裡也允許任何允許的標量 values)。

4.5.3 更多例子

您可能還想在 new 中使用允許的屬性 action。這就提出了一個問題,你不能使用 require 在 root key 因為通常在呼叫 new 時它不存在:

# 使用 `fetch` 你可以提供一個預設值並使用
# 來自那裡的強引數 API。
params.fetch(:blog, {}).permit(:title, :author)

model 類方法 accepts_nested_attributes_for 允許您 更新和銷燬相關記錄。這是基於 id_destroy 引數:

# 允許 :id 和 :_destroy
params.require(:author).permit(:name, books_attributes: [:title, :id, :_destroy])

具有整數 keys 的雜湊被差別對待,您可以宣告 屬性就好像他們是直系孩子一樣。你得到這些 組合使用 accepts_nested_attributes_for 時的引數 使用 has_many association:

# 允許以下資料:
# {"book" => {"title" => "Some Book",
# "chapters_attributes" => { "1" => {"title" => "第一章"},
# "2" => {"title" => "第二章"}}}}

params.require(:book).permit(:title, chapters_attributes: [:title])

想象一下您有代表產品的引數的場景 名稱,以及與該產品關聯的任意資料的雜湊值,以及 您想允許產品名稱屬性以及整個 資料雜湊:

def product_params
  params.require(:product).permit(:name, data: {})
end
4.5.4 強引數範圍外

強引數 API 是針對最常見的用例設計的 心裡。這並不是處理所有問題的靈丹妙藥 引數過濾問題。但是,您可以輕鬆地將 API 與您的 自己的程式碼以適應您的情況。

5 Session

您的應用程式為每個使用者都有一個 session,您可以在其中儲存將在請求之間持久化的少量資料。 session 僅在 controller 和 view 中可用,並且可以使用幾種不同的儲存機制之一:

所有的 session 商店都使用 cookie 來儲存每個 session 的唯一 ID(您必須使用 cookie,Rails 不允許您在 URL 中傳遞 session ID,因為這不太安全)。

對於大多數商店,此 ID 用於在伺服器上查詢 session 資料,例如在資料庫表中。有一個例外,那就是預設和推薦的 session 儲存 - CookieStore - 它將所有 session 資料儲存在 cookie 本身中(如果您需要該 ID,您仍然可以使用它)。這具有非常輕量級的優點,並且需要在新應用程式中進行零設定才能使用 session。 cookie 資料經過加密簽名以防止篡改。它也是加密的,因此任何有權訪問它的人都無法讀取其內容。 (如果它被編輯過,Rails 將不會接受它)。

CookieStore 可以儲存大約 4 kB 的資料——比其他的要少得多——但這通常就足夠了。無論您的應用程式使用哪個 session 儲存,都不鼓勵在 session 中儲存大量資料。您應該特別避免在 session 中儲存複雜的物件(例如 model 實例),因為伺服器可能無法在請求之間重新組裝它們,這將導致錯誤。

如果您的使用者 sessions 不儲存關鍵資料或不需要長時間待在附近(例如,如果您只是使用 flash 進行訊息傳遞),則可以考慮使用 ActionDispatch::Session::CacheStore。這將使用您為應用程式配置的快取實現儲存 sessions。這樣做的好處是您可以使用現有的快取基礎架構來儲存 sessions,而無需任何額外的設定或管理。當然,缺點是 sessions 將是短暫的並且可能隨時消失。

安全指南 中閱讀有關 session 儲存的更多資訊。

如果您需要不同的 session 儲存機制,您可以在初始化程式中更改它:

# 使用資料庫為 sessions 而不是基於 cookie 的預設值,
# 不應該用於儲存高度機密的資訊
# (使用 "rails g active_record:session_migration" 建立 session 表)
# Rails.application.config.session_store :active_record_store

Rails 在簽署 session 資料時設定了一個 session key(cookie 的名稱)。這些也可以在初始化程式中更改:

# 修改這個檔案的時候一定要重啟伺服器。
Rails.application.config.session_store :cookie_store, key: '_your_app_session'

也可以傳入一個 :domain key 併為 cookie 指定域名:

# 修改這個檔案的時候一定要重啟伺服器。
Rails.application.config.session_store :cookie_store, key: '_your_app_session', domain: ".example.com"

Rails 為 CookieStore 設定了一個秘密 key,用於對 config/credentials.yml.enc 中的 session 資料進行簽名。這可以透過 bin/rails credentials:edit 更改。

# aws:
# access_key_id: 123
#secret_access_key:345

# 用作 Rails 中所有 MessageVerifiers 的基本秘密,包括保護 cookies 的秘密。
secret_key_base: 492f...

在使用 CookieStore 時更改 secret_key_base 將使所有現有的 sessions 無效。

5.1 訪問 Session

在你的 controller 中,你可以透過 session 實例方法訪問 session。

Session 是延遲載入的。如果您不在 action 的程式碼中訪問會話,它們將不會被載入。因此,您永遠不需要禁用會話,只要不訪問它們就可以完成這項工作。

Session values 使用 key/值對像雜湊一樣儲存:

class ApplicationController < ActionController::Base

  private

  # Finds the User with the ID stored in the session with the key
  # :current_user_id 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 ||= session[:current_user_id] &&
      User.find_by(id: session[:current_user_id])
  end
end

要將某些內容儲存在 session 中,只需將其分配給 key 就像一個雜湊:

class LoginsController < ApplicationController
  # "Create" a login, aka "log the user in"
  def create
    if user = User.authenticate(params[:username], 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

要從 session 中刪除某些內容,請刪除 key/value 對:

class LoginsController < ApplicationController
  # "Delete" a login, aka "log the user out"
  def destroy
    # Remove the user id from the session
    session.delete(:current_user_id)
    # Clear the memoized current user
    @_current_user = nil
    redirect_to root_url
  end
end

要重置整個 session,請使用 reset_session

5.2 Flash

flash 是 session 的一個特殊部分,每次請求都會清除它。這意味著儲存在那裡的 values 只會在下一個請求中可用,這對於傳遞錯誤訊息等很有用。

透過 flash 方法訪問快閃記憶體。與 session 一樣,快閃記憶體表示為雜湊。

讓我們以登出行為為例。 controller 可以傳送一條訊息,該訊息將在下一個請求時顯示給使用者:

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

請注意,也可以將 flash 訊息分配為重定向的一部分。您可以分配 :notice:alert 或通用 :flash

redirect_to root_url, notice: "You have successfully logged out."
redirect_to root_url, alert: "You're stuck here!"
redirect_to root_url, flash: { referral_code: 1234 }

destroy action 重定向到應用程式的 root_url,訊息將在此處顯示。請注意,完全由下一個 action 來決定它會如何處理前一個 action 放入快閃記憶體中的內容。通常在應用程式的佈局中顯示來自 Flash 的任何錯誤警報或通知:

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

    <!-- more content -->
  </body>
</html>

這樣,如果 action 設定了通知或警報訊息,佈局將自動顯示它。

您可以傳遞 session 可以儲存的任何內容;您不僅限於通知和警報:

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

如果您希望將 Flash value 轉移到另一個請求,請使用 flash.keep:

class MainController < ApplicationController
  # Let's say this action corresponds to root_url, but you want
  # all requests here to be redirected to UsersController#index.
  # If an action sets the flash and redirects here, the values
  # would normally be lost when another redirect happens, but you
  # can use 'keep' to make it persist for another request.
  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
5.2.1 flash.now

預設情況下,將 values 新增到 flash 將使它們可用於下一個請求,但有時您可能希望在同一請求中訪問那些 values。例如,如果 create action 無法儲存資源,而您直接渲染 new 模板,則不會導致新請求,但您可能仍希望使用 flash 顯示訊息。為此,您可以像使用普通的 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 Cookies

您的應用程式可以在客戶端上儲存少量資料 - 稱為 cookies - 這些資料將跨請求甚至 sessions 保持。 Rails 透過 cookies 方法提供對 cookies 的輕鬆訪問,該方法 - 與 session 非常相似 - 像雜湊一樣工作:

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
      flash[:notice] = "Thanks for your comment!"
      if params[:remember_name]
        # Remember the commenter's name.
        cookies[:commenter_name] = @comment.author
      else
        # Delete cookie for the commenter's name cookie, if any.
        cookies.delete(:commenter_name)
      end
      redirect_to @comment.article
    else
      render action: "new"
    end
  end
end

請注意,對於 session values,您可以將 key 設定為 nil,要刪除 cookie 值,您應該使用 cookies.delete(:key)

Rails 還提供了一個簽名的 cookie jar 和一個加密的 cookie jar 用於儲存 敏感資料。已簽名的 cookie jar 在 cookie values 來保護它們的完整性。加密後的 cookie jar 加密 values 除了對它們進行簽名外,終端使用者無法讀取它們。 參考API文件 更多細節。

這些特殊的 cookie jar 使用序列化程式將分配的 values 序列化為 字串並在讀取時將它們反序列化為 Ruby 物件。

您可以指定要使用的序列化程式:

Rails.application.config.action_dispatch.cookies_serializer = :json

新應用程式的預設序列化程式是 :json。為了相容 已有cookies的舊應用,當serializer時使用:marshal 未指定選項。

您也可以將此選項設定為 :hybrid,在這種情況下 Rails 將透明地 讀取時反序列化現有的(Marshal 序列化的)cookies 並將它們重新寫入 JSON 格式。這對於將現有應用程式遷移到 :json 序列化程式。

也可以傳遞回應 load 的自定義序列化程式和 dump

Rails.application.config.action_dispatch.cookies_serializer = MyCustomSerializer

使用 :json:hybrid 序列化程式時,您應該注意並非所有 Ruby 物件可以序列化為 JSON。例如,DateTime 物件 將被序列化為字串,並且 Hashes 將其 keys 字串化。

class CookiesController < ApplicationController
  def set_cookie
    cookies.encrypted[:expiration_date] = Date.tomorrow # => Thu, 20 Mar 2014
    redirect_to action: 'read_cookie'
  end

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

建議您只在 cookies 中儲存簡單資料(字串和數字)。 如果必須儲存複雜的物件,則需要處理轉換 在後續請求中讀取 values 時手動。

如果您使用 cookie session 儲存,這將適用於 sessionflash 雜湊也是如此。

7 呈現 XML 和 JSON 資料

ActionController 使渲染 XML 或 ActionController 資料變得非常容易。如果您使用 scaffolding 生成了 controller,它看起來像這樣:

class UsersController < ApplicationController
  def index
    @users = User.all
    respond_to do |format|
      format.html # index.html.erb
      format.xml  { render xml: @users }
      format.json { render json: @users }
    end
  end
end

您可能會注意到在上面的程式碼中我們使用的是 render xml: @users,而不是 render xml: @users.to_xml。如果物件不是字串,那麼Rails 會自動為我們呼叫to_xml

8 過濾器

過濾器是在 controller action“之前”、“之後”或“周圍”執行的方法。

過濾器是繼承的,因此如果您在 ApplicationController 上設定過濾器,它將在應用程式中的每個 controller 上執行。

“之前”過濾器透過 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

該方法只是在 flash 中儲存一條錯誤訊息,如果使用者未登入,則重定向到登入表單。如果“before”過濾器呈現或重定向,則 action 將不會執行。如果有其他過濾器計劃在該過濾器之後執行,它們也會被取消。

在這個例子中,過濾器被新增到 ApplicationController,因此應用程式中的所有 controllers 都繼承它。這將使應用程式中的所有內容都需要使用者登入才能使用它。出於顯而易見的原因(使用者首先無法登入!),並非所有 controllers 或 actions 都需要這樣做。您可以使用 skip_before_action 阻止此過濾器在特定 actions 之前執行:

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

現在,LoginsControllernewcreate actions 將像以前一樣工作,無需使用者登入。 :only 選項用於僅對這些 actions 跳過此過濾器,還有另一種方式的 ZTHZW_4 工作方式選項。這些選項也可以在新增過濾器時使用,因此您可以首先新增一個僅對選定的 actions 執行的過濾器。

使用不同的選項多次呼叫相同的過濾器將不起作用, 因為最後一個過濾器定義將覆蓋以前的過濾器定義。

8.1 過濾器後和過濾器周圍

除了“之前”過濾器之外,您還可以在執行 action 之後或之前和之後執行過濾器。

“之後”過濾器透過 after_action 註冊。它們類似於“before”過濾器,但由於 action 已經執行,它們可以訪問即將傳送到客戶端的回應資料。顯然,“after”過濾器無法阻止 action 執行。請注意,“after”過濾器僅在成功的 action 之後執行,而不是在請求週期中引發異常時執行。

“周圍”過濾器透過 around_action 註冊。它們負責透過讓步來執行關聯的 actions,類似於 Rack 中介軟體的工作方式。

例如,在更改具有批准工作流的網站中,管理員可以透過在 transaction 中應用它們輕鬆地預先設定它們:

class ChangesController < ApplicationController
  around_action :wrap_in_transaction, only: :show

  private

  def wrap_in_transaction
    ActiveRecord::Base.transaction do
      begin
        yield
      ensure
        raise ActiveRecord::Rollback
      end
    end
  end
end

請注意,“周圍”過濾器也會包裝渲染。特別是,在上面的例子中,如果 view 本身從資料庫中讀取(例如透過範圍),它會在 transaction 中這樣做,從而將資料呈現給 preview。

您可以選擇不讓步並自行構建回應,在這種情況下,將不會執行 action。

8.2 其他使用過濾器的方法

雖然使用過濾器的最常見方法是建立私有方法並使用 before_actionafter_actionaround_action 新增它們,但還有兩種其他方法可以做同樣的事情。

第一種是直接透過 *_action 方法使用塊。該塊接收 controller 作為引數。上面的 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? 方法是私有的,並且過濾器不在 controller 的範圍內執行。這不是實現此特定過濾器的推薦方法,但在更簡單的情況下,它可能很有用。

特別是對於 around_action,該塊也在 action 中產生:

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

第二種方法是使用類(實際上,任何回應正確方法的物件都可以)來處理過濾。這在更復雜且無法使用其他兩種方法以可讀和可重用的方式實現的情況下很有用。例如,您可以再次重寫登入過濾器以使用一個類:

class ApplicationController < ActionController::Base
  before_action LoginFilter
end

class LoginFilter
  def self.before(controller)
    unless controller.send(:logged_in?)
      controller.flash[:error] = "You must be logged in to access this section"
      controller.redirect_to controller.new_login_url
    end
  end
end

同樣,這不是此過濾器的理想示例,因為它不在 controller 的範圍內執行,而是將 controller 作為引數傳遞。過濾器類必須實現一個與過濾器同名的方法,所以對於before_action過濾器,該類必須實現一個before方法,依此類推。 around 方法必須是 yield 才能執行 action。

9 請求偽造保護

跨站點請求偽造是一種攻擊型別,在這種攻擊中,站點誘使使用者在另一個站點上發出請求,可能會在使用者不知情或未經使用者許可的情況下新增、修改或刪除該站點上的資料。

避免這種情況的第一步是確保所有“破壞性”的 actions(建立、更新和銷燬)只能透過非 GET 請求訪問。如果您遵循 RESTful 約定,那麼您已經在這樣做了。但是,惡意站點仍然可以很容易地向您的站點發送非 GET 請求,這就是請求偽造保護的用武之地。顧名思義,它可以防止偽造請求。

這樣做的方法是為每個請求新增一個不可猜測的 token,它只有您的伺服器知道。這樣,如果請求沒有正確的 token 進入,它將被拒絕訪問。

如果生成這樣的表單:

<%= form_with model: @user do |form| %>
  <%= form.text_field :username %>
  <%= form.text_field :password %>
<% end %>

您將看到 token 如何被新增為隱藏欄位:

<form accept-charset="UTF-8" action="/users/1" method="post">
<input type="hidden"
       value="67250ab105eb5ad10851c00a5621854a23af5489"
       name="authenticity_token"/>
<!-- fields -->
</form>

Rails 將此 token 新增到使用 form helpers 生成的每個表單中,因此大多數時候您不必擔心它。如果您正在手動編寫表單或出於其他原因需要新增 token,則可以透過方法 form_authenticity_token 獲得它:

form_authenticity_token 生成有效的認證 token。這在 Rails 不會自動新增它的地方很有用,比如在自定義 Ajax 呼叫中。

安全指南 有更多關於這方面的內容,以及在開發 Web 應用程式時您應該注意的許多其他與安全相關的問題。

10 請求和回應物件

在每個 controller 中,有兩個訪問器方法指向與當前正在執行的請求週期關聯的請求和回應物件。 request 方法包含 ActionDispatch::Request 的實例,response 方法返回一個回應物件,表示將要傳送回客戶端的內容。

10.1 request 物件

request 物件包含許多關於來自客戶端的請求的有用資訊。要獲取可用方法的完整列表,請參閱 Rails API 文件 和 [機架文件](https://www.rubydoc .info/github/rack/rack/Rack/Request)。您可以在此物件上訪問的屬性包括:

request 的屬性 目的
host 用於此請求的主機名。
domain(n=2) 主機名的第一個 n 段,從右側(TLD)開始。
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。
10.1.1 path_parametersquery_parametersrequest_parameters

Rails 在 params 雜湊中收集與請求一起傳送的所有引數,無論它們是作為查詢字串的一部分發送的,還是作為帖子正文的一部分發送的。 request 物件有三個訪問器,根​​據它們的來源,您可以訪問這些引數。 query_parameters 雜湊包含作為查詢字串的一部分發送的引數,而 request_parameters 雜湊包含作為帖子正文的一部分發送的引數。 path_parameters 雜湊包含路由識別為通向此特定 controller 和 action 的路徑的一部分的引數。

10.2 response 物件

回應物件通常不直接使用,而是在執行操作和呈現傳送回用戶的資料期間構建,但有時 - 就像在後過濾器中 - 訪問回應可能很有用直接地。其中一些訪問器方法也有設定器,允許您更改它們的 values。要獲取可用方法的完整列表,請參閱 Rails API 文件 和 [機架文件](https://www.rubydoc .info/github/rack/rack/Rack/Response)。

response 的屬性 目的
body 這是傳送回客戶端的資料字串。這通常是 HTML。
status 回應的 HTTP 狀態程式碼,例如 200 表示成功請求或 404 表示未找到檔案。
location 客戶端被重定向到的 URL(如果有)。
content_type 回應的內容型別。
charset 用於回應的字符集。預設為“utf-8”。
headers 用於回應的標頭。
10.2.1 設定自定義標題

如果你想為回應設定自定義標頭,那麼 response.headers 就是這樣做的地方。 headers 屬性是一個雜湊值,它將標題名稱對映到它們的 values,Rails 會自動設定其中的一些。如果要新增或更改標題,只需將其分配給 response.headers 即可:

response.headers["Content-Type"] = "application/pdf"

在上述情況下,直接使用 content_type 設定器會更有意義。

11 HTTP 身份驗證

Rails 帶有三種內建的 HTTP 身份驗證機制:

  • 基本認證
  • 摘要認證
  • Token 認證

11.1 HTTP 基本認證

HTTP 基本身份驗證是大多數瀏覽器和其他 HTTP 客戶端支援的身份驗證方案。例如,考慮一個管理部分,該部分只能透過在瀏覽器的 HTTP 基本對話視窗中輸入使用者名稱和密碼來訪問。使用內建身份驗證非常簡單,只需要您使用一種方法,http_basic_authenticate_with

class AdminsController < ApplicationController
  http_basic_authenticate_with name: "humbaba", password: "5baa61e4"
end

有了這個,您可以建立從 AdminsController 繼承的名稱空間 controllers。因此,過濾器將對那些 controllers 中的所有 actions 執行,並使用 HTTP 基本身份驗證保護它們。

11.2 HTTP 摘要認證

HTTP 摘要身份驗證優於基本身份驗證,因為它不需要客戶端透過網路傳送未加密的密碼(儘管 HTTP 基本身份驗證在 HTTPS 上是安全的)。在 Rails 中使用摘要認證非常簡單,只需要使用一種方法,authenticate_or_request_with_http_digest

class AdminsController < ApplicationController
  USERS = { "lifo" => "world" }

  before_action :authenticate

  private
    def authenticate
      authenticate_or_request_with_http_digest do |username|
        USERS[username]
      end
    end
end

如上例所示,authenticate_or_request_with_http_digest 塊只接受一個引數 - 使用者名稱。並且塊返回密碼。從 authenticate_or_request_with_http_digest 返回 falsenil 會導致認證失敗。

11.3 HTTP Token 認證

HTTP 令牌認證是一種啟用在 HTTP Authorization 標頭中使用 Bearer tokens 的方案。有許多可用的令牌格式,對它們的描述超出了本文件的範圍。

舉個例子,假設您要使用預先發布的認證token來進行認證和訪問。使用 Rails 實現 token 身份驗證非常簡單,只需要使用一種方法,authenticate_or_request_with_http_token

class PostsController < ApplicationController
  TOKEN = "secret"

  before_action :authenticate

  private
    def authenticate
      authenticate_or_request_with_http_token do |token, options|
        ActiveSupport::SecurityUtils.secure_compare(token, TOKEN)
      end
    end
end

如上例所示,authenticate_or_request_with_http_token 塊採用兩個引數 - token 和 Hash 包含從 HTTP Authorization 標頭解析的選項。如果身份驗證成功,該塊應返回 true。在其上返回 falsenil 將導致身份驗證失敗。

12 流媒體和檔案下載

有時您可能希望向使用者傳送檔案而不是呈現 HTML 頁面。 Rails 中的所有 controllers 都有 send_datasend_file 方法,它們都將資料流傳輸到客戶端。 send_file 是一種方便的方法,可讓您提供磁碟上檔案的名稱,它會為您流式傳輸該檔案的內容。

要將資料流式傳輸到客戶端,請使用 send_data

require "prawn"
class ClientsController < ApplicationController
  # Generates a PDF document with information on the client and
  # returns it. The user will get the PDF as a file download.
  def download_pdf
    client = Client.find(params[:id])
    send_data generate_pdf(client),
              filename: "#{client.name}.pdf",
              type: "application/pdf"
  end

  private
    def generate_pdf(client)
      Prawn::Document.new do
        text client.name, align: :center
        text "Address: #{client.address}"
        text "Email: #{client.email}"
      end.render
    end
end

上例中的 download_pdf action 將呼叫一個私有方法,該方法實際生成 PDF 文件並將其作為字串返回。然後,該字串將作為檔案下載流式傳輸到客戶端,並向用戶建議檔名。有時,當向用戶流式傳輸檔案時,您可能不希望他們下載檔案。以可以嵌入 HTML 頁面的影象為例。要告訴瀏覽器不打算下載某個檔案,您可以將 :disposition 選項設定為“inline”。此選項的相反和預設 value 是“附件”。

12.1 傳送檔案

如果要傳送磁碟上已存在的檔案,請使用 send_file 方法。

class ClientsController < ApplicationController
  # Stream a file that has already been generated and stored on disk.
  def download_pdf
    client = Client.find(params[:id])
    send_file("#{Rails.root}/files/clients/#{client.id}.pdf",
              filename: "#{client.name}.pdf",
              type: "application/pdf")
  end
end

這將一次讀取和流式傳輸 4 kB 的檔案,避免將整個檔案一次載入到記憶體中。您可以使用 :stream 選項關閉流式傳輸或使用 :buffer_size 選項調整塊大小。

如果未指定 :type,則根據 :filename 中指定的副檔名猜測。如果沒有為擴充套件註冊內容型別,則將使用 application/octet-stream

警告:使用來自客戶端的資料(引數、cookies 等)在磁碟上定位檔案時要小心,因為這是一個安全風險,可能允許某人訪問他們不打算訪問的檔案。

提示:如果您可以將靜態檔案儲存在 Web 伺服器上的公共資料夾中,則不建議您透過 Rails 流式傳輸它們。讓使用者直接使用 Apache 或其他 Web 伺服器下載檔案效率更高,避免請求不必要地透過整個 Rails 堆疊。

12.2 RESTful 下載

雖然 send_data 工作得很好,但如果您正在建立具有單獨的 actions 用於檔案下載的 RESTful 應用程式,則通常沒有必要。在 REST 術語中,上述示例中的 PDF 檔案可以被視為客戶端資源的另一種表示。 Rails 提供了一種簡單而時尚的“RESTful 下載”方式。以下是如何重寫示例,以便 PDF 下載是 show 操作的一部分,沒有任何流:

class ClientsController < ApplicationController
  # The user can request to receive this resource as HTML or PDF.
  def show
    @client = Client.find(params[:id])

    respond_to do |format|
      format.html
      format.pdf { render pdf: generate_pdf(@client) }
    end
  end
end

為了讓這個示例工作,您必須將 PDF MIME 型別新增到 Rails。這可以透過將以下行新增到檔案 config/initializers/mime_types.rb 來完成:

Mime::Type.register "application/pdf", :pdf

配置檔案不會在每次請求時重新載入,因此您必須重新啟動伺服器才能使更改生效。

現在,使用者只需將“.pdf”新增到 URL 即可請求獲取客戶端的 PDF 版本:

GET /clients/1.pdf

12.3 任意資料的直播

Rails 允許您傳輸的不僅僅是檔案。實際上,您可以流式傳輸任何內容 你想要一個回應物件。 ActionController::Live module 允許 您可以建立與瀏覽器的持久連線。使用此 module,您將 能夠在特定時間點向瀏覽器傳送任意資料。

12.3.1 整合直播

在 controller 類中包含 ActionController::Live 將提供 controller 裡面的所有 actions 都具有流式傳輸資料的能力。你可以混進去 module 像這樣:

class MyController < ActionController::Base
  include ActionController::Live

  def stream
    response.headers['Content-Type'] = 'text/event-stream'
    100.times {
      response.stream.write "hello world\n"
      sleep 1
    }
  ensure
    response.stream.close
  end
end

上面的程式碼會與瀏覽器保持持久連線併發送 100 "hello world\n" 的訊息,每隔一秒。

在上面的例子中有幾件事情需要注意。我們需要做 確保關閉回應流。忘記關閉流將離開 socket 永遠開放。我們還必須將內容型別設定為 text/event-stream 在我們寫入回應流之前。這是因為無法寫入標頭 回應提交後(當 response.committed? 返回一個真值時 value),當你使用 writecommit 回應流時會發生這種情況。

12.3.2 示例用法

假設您正在製作卡拉 OK 機,並且使用者想要獲得 特定歌曲的歌詞。每個 Song 都有特定數量的行和 每一行都需要時間 num_beats 來完成唱歌。

如果我們想以卡拉 OK 方式返回歌詞(僅在 歌手已經完成了上一行),那麼我們可以使用ActionController::Live 如下:

class LyricsController < ActionController::Base
  include ActionController::Live

  def show
    response.headers['Content-Type'] = 'text/event-stream'
    song = Song.find(params[:id])

    song.each do |line|
      response.stream.write line.lyrics
      sleep line.num_beats
    end
  ensure
    response.stream.close
  end
end

上面的程式碼只有在歌手完成了上一行之後才會傳送下一行 線。

12.3.3 流媒體注意事項

流式傳輸任意資料是一種非常強大的工具。如上圖所示 例如,您可以選擇在回應流中傳送的時間和內容。然而, 您還應該注意以下事項:

  • 每個回應流建立一個新執行緒並複製本地執行緒 來自原始執行緒的變數。執行緒區域性變數過多會導致 對效能產生負面影響。同樣,大量執行緒也可以 阻礙效能。
  • 未能關閉回應流將保持對應的 socket 開啟 永遠。確保在使用回應流時呼叫 close
  • WEBrick 伺服器緩衝所有回應,因此包括 ActionController::Live 不管用。您必須使用不會自動緩衝的網路伺服器 回應。

13 日誌過濾

Rails 在 log 資料夾中為每個環境儲存一個日誌檔案。這些在除錯應用程式中實際發生的事情時非常有用,但在實時應用程式中,您可能不希望將所有資訊都儲存在日誌檔案中。

13.1 引數過濾

您可以透過將敏感請求引數附加到應用程式配置中的 config.filter_parameters 來從日誌檔案中過濾掉敏感的請求引數。這些引數將在日誌中標記為 [FILTERED]。

config.filter_parameters << :password

提供的引數將被部分匹配的正則表示式過濾掉。 Rails 在適當的初始化器(initializers/filter_parameter_logging.rb)中添加了預設的 :password,並關心典型的應用程式引數 passwordpassword_confirmation

13.2 重定向過濾

有時需要從日誌檔案中過濾掉應用程式重定向到的一些敏感位置。 您可以使用 config.filter_redirect 配置選項來做到這一點:

config.filter_redirect << 's3.amazonaws.com'

您可以將其設定為字串、正則表示式或兩者的陣列。

config.filter_redirect.concat ['s3.amazonaws.com', /private_path/]

匹配的 URL 將被標記為“[過濾]”。

14 救援

很可能您的應用程式將包含錯誤或以其他方式丟擲需要處理的異常。例如,如果使用者點選了指向資料庫中不再存在的資源的連結,Active Record 將丟擲 ActiveRecord::RecordNotFound 異常。

Rails 預設的異常處理會為所有異常顯示“500 伺服器錯誤”訊息。如果請求是在本地發出的,則會顯示一個很好的回溯和一些附加資訊,因此您可以找出問題所在並進行處理。如果請求是遠端的,Rails 只會向用戶顯示一個簡單的“500 伺服器錯誤”訊息,或者如果存在路由錯誤或找不到記錄,則顯示“404 Not Found”。有時您可能想要自定義如何捕獲這些錯誤以及如何向用戶顯示這些錯誤。 Rails 應用程式中有幾個級別的異常處理可用:

14.1 預設的 500 和 404 模板

預設情況下,在生產環境中,應用程式將呈現 404 或 500 錯誤訊息。在開發環境中,所有未處理的異常都會被簡單地引發。這些訊息包含在 public 資料夾中的靜態 HTML 檔案中,分別位於 404.html500.html 中。您可以自定義這些檔案以新增一些額外的資訊和樣式,但請記住它們是靜態 HTML;也就是說,您不能為它們使用 ERB、SCSS、CoffeeScript 或佈局。

14.2 rescue_from

如果你想在捕獲錯誤時做一些更復雜的事情,你可以使用 rescue_from,它處理整個 controller 及其子類中某種型別(或多種型別)的異常。

當發生由 rescue_from 指令捕獲的異常時,異常物件將傳遞給處理程式。處理程式可以是傳遞給 :with 選項的方法或 Proc 物件。您也可以直接使用塊而不是顯式的 Proc 物件。

以下是如何使用 rescue_from 攔截所有 ActiveRecord::RecordNotFound 錯誤並對其進行處理的方法。

class ApplicationController < ActionController::Base
  rescue_from ActiveRecord::RecordNotFound, with: :record_not_found

  private
    def record_not_found
      render plain: "404 Not Found", status: 404
    end
end

當然,這個例子並不複雜,根本沒有改進預設的異常處理,但是一旦你能捕獲所有這些異常,你就可以自由地對它們做任何你想做的事情。例如,您可以建立自定義異常類,當用戶無權訪問應用程式的某個部分時將丟擲這些異常類:

class ApplicationController < ActionController::Base
  rescue_from User::NotAuthorized, with: :user_not_authorized

  private
    def user_not_authorized
      flash[:error] = "You don't have access to this section."
      redirect_back(fallback_location: root_path)
    end
end

class ClientsController < ApplicationController
  # Check that the user has the right authorization to access clients.
  before_action :check_authorization

  # Note how the actions don't have to worry about all the auth stuff.
  def edit
    @client = Client.find(params[:id])
  end

  private
    # If the user is not authorized, just throw the exception.
    def check_authorization
      raise User::NotAuthorized unless current_user.admin?
    end
end

警告:將 rescue_fromExceptionStandardError 一起使用會導致嚴重的副作用,因為它會阻止 Rails 正確處理異常。因此,除非有充分理由,否則不建議這樣做。

在生產環境中執行時,所有 ActiveRecord::RecordNotFound 錯誤呈現 404 錯誤頁面。除非你需要 您不需要處理此自定義行為。

某些異常只能從 ApplicationController 類中拯救出來,因為它們在 controller 被初始化之前被引發,並且 action 被執行。

15 強制HTTPS協議

如果您想確保只能與您的 controller 進行通訊 透過 HTTPS,您應該透過啟用 ActionDispatch::SSL 中介軟體來實現 config.force_ssl 在您的環境配置中。

回饋

我們鼓勵您幫助提高本指南的品質。

如果您發現任何拼寫錯誤或資訊錯誤,請提供回饋。 要開始回饋,您可以閱讀我們的 回饋 部分。

您還可能會發現不完整的內容或不是最新的內容。 請務必為 main 新增任何遺漏的文件。假設是 非穩定版指南(edge guides) 請先驗證問題是否已經在主分支上解決。 請前往 Ruby on Rails 指南寫作準則 查看寫作風格和慣例。

如果由於某種原因您發現要修復的內容但無法自行修補,請您 提出 issue

關於 Ruby on Rails 的任何類型的討論歡迎提供任何文件至 rubyonrails-docs 討論區