# 5.1 控制器中的請求和相應
## 概要
本課時講解控制器中如何處理傳入的參數和相應,并且介紹在請求和相應的過程中,如何處理請求參數,使用 sesson,設置 etag 緩存和使用 csrf 確保數據來源安全。
## 知識點
- request
- response
- params
- respond_to
- session
- cookies
- etag
- csrf
## 正文
### 5.1.1 Action Pack
[Action Pack](https://github.com/rails/rails/tree/master/actionpack) 是 Rails 種又一個核心的 Gem,它可以處理 web 請求,使用 routes 中定義的規則調用控制器(Controller)及方法(Action),并且自動判斷請求類型,做出對應的相應。
Rails 中的控制器,指的就是處理請求及做出相應。
### 5.1.2 Request 類
`ActionDispatch::Request` 類是對 web 請求的包裝類,它有兩個常用的方法:
~~~
request.headers["Content-Type"] # => "text/plain"
~~~
`headers` 包含了請求的頭信息。
~~~
request.parameters
~~~
它會返回請求的參數,不過我們并不直接使用它,而是使用 `params` 方法獲得,這在稍后介紹。
Request 類的源代碼在[這里](https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/http/request.rb)。
### 5.1.3 Response 類
`ActionDispatch::Response` 類代表了響應結果,它也有常用的方法,不過我們更經常用的是 Controller 中的 action 和回調。在一些測試代碼中,我們經常使用 response 實例。
比如,我們測試商品刪除之后,會返回到商品列表,我們的測試代碼是:
~~~
RSpec.describe ProductsController, type: :controller do
...
describe "DELETE #destroy" do
it "redirects to the products list" do
product = Product.create! valid_attributes
delete :destroy, {:id => product.to_param}, valid_session
expect(response).to redirect_to(products_url)
end
end
end
~~~
Request 和 Response 在我們的業務邏輯代碼中并不不常用到,下面介紹的內容,是我們在編寫控制器代碼時,經常遇到的。
### 5.1.4 strong paramaters
Controller 是控制器的概念,所謂控制,指在網絡傳輸中,接收參數和做出相應。Controller 有兩種方式接收參數:GET 和 POST。兩種方式均可通過 `params` 讀取傳遞的內容。
在 Rails3之前的版本中,當接收傳遞的參數,用來更新資源屬性時,可以設定 Model 的屬性白名單,非報名單上的屬性不允許通過參數傳遞的方式修改,比如:
~~~
class User < ActiveRecord::Base
attr_accessible :name
end
~~~
在 Rails 4 之后,這個方法轉為 [gem](https://github.com/rails/protected_attributes),不再是 Rails 4 的核心功能,但將在 Rails 5 中重新回到核心功能中。現在,使用 `permit` 方法來過濾參數。使用 scaffold 創建的 Controller 默認使用了該方法:
~~~
class ProductsController < ApplicationController
def create
@product = Product.new(product_params)
...
private
def product_params
params.require(:product).permit(:name, :price, :description)
end
end
~~~
`permit` 可以設定關聯關系的屬性:
~~~
params.require(:product).permit(:name, :price, :description, variants_attributes: [:price, :size, :id, :_destroy])
~~~
`:id` 和 `:_destroy` 適用于上一章介紹的 `accepts_nested_attributes_for` 方法。
### 5.1.5 respond_to 方法
Controller 響應請求有多種結果,響應返回 `Status Code`,常見的有 200(成功響應),302(跳轉),404(未找到資源),500(內部錯誤)。更多響應 Code 參考 [3.3 視圖中的 AJAX 交互](#)。
一個 controller 的 action 對應一個請求,這樣可以保持我們業務邏輯代碼清晰,易維護。一個 action 可以響應一個請求的多中類型,這在我們第三章里已經有了介紹和演示。
Controller 使用 `respond_to` 方法,針對每一種請求類型,做出響應:
~~~
respond_to do |format|
if @product.save
format.html { redirect_to @product, notice: 'Product was successfully created.' }
format.json { render :show, status: :created, location: @product }
else
format.html { render :new }
format.json { render json: @product.errors, status: :unprocessable_entity }
end
format.js
end
~~~
當我們處理多個資源時,每個資源的 `create` 和 `update` 等資源方法,大多都具備相同的邏輯代碼。除了特定的業務邏輯,他們都會響應典型的資源操作。 Rails 4.2 之前提供了 `respond_with` 訪問,4.2 之后將它轉為一個 gem,我們安裝這個 gem:
~~~
gem "responders"
~~~
并且創建文件:
~~~
% rails g responders:install
create lib/application_responder.rb
insert config/application.rb
prepend app/controllers/application_controller.rb
insert app/controllers/application_controller.rb
create config/locales/responders.en.yml
~~~
默認,它只支持 :html,因為我們演示時,又使用到了 :json 和 :js,還有 :xml,我們將這些類型添加上:
~~~
class ApplicationController < ActionController::Base
self.responder = ApplicationResponder
respond_to :html, :xml, :json, :js
~~~
我們將剛才 `respond_to` 方法改成 `respond_with`,精簡重復的代碼(Dry up your code):
~~~
def create
@product = Product.create(product_params)
respond_with(@product)
end
~~~
在 [6.4 I18n](#) 中,我們講 I18n 文件做了整理,這里我們把 generator 創建的語言包,按照 6.4 一節中介紹的方式進行管理,并且增加中文提示。如此,我們不必為每個資源創建、修改等操作各自編寫語言提示了。
### 5.1.6 session 和 cookies
從一個請求到另一個請求,Rails 使用 Session 來保存一些簡單的信息,比如 user_id 等。同時,也可以用 cookies 保存該信息。
當 Rails 項目創建的時候,它會有一個默認的 cookie name,這在 `config/initializers/session_store.rb` 中:
~~~
Rails.application.config.session_store :cookie_store, key: '_rails-practice_session'
~~~
這里,我們用 `cookie_store` 來儲存 session,當我們在項目中保存 session 的時候,數據會保存在這個 cookie 中。

在 Rails 2 之前,可以 decode 這個內容,查看其中 session 的內容:
~~~
require 'rack'
cookie = "WmQyNFliZnprd3..."
Rack::Session::Cookie::Base64::Marshal.new.decode(cookie)
=> {"session_id"=>"d3b17...", "user_id"=>"123", "_csrf_token"=>"rtkofT..."}
~~~
因為在 Rails 3 中已經增加了 `secret_key_base`,所以無法直接 decode 內容了。
但是,如果單獨使用一個 cookie 來記錄數據,默認是不經過任何加密的:
~~~
cookies[:name] = "Rails"
~~~

如果這個數據不想被暴露,需要單獨加密:
~~~
cookies.signed[:name] = "Rails"
cookies.permanent.signed[:name] = "Rails" [1]
~~~
`permanent` 會讓這個 cookie 有20年的有效時間。
Cookie 的 [api](http://api.rubyonrails.org/classes/ActionDispatch/Cookies.html) 文檔在這里。
如果我們在 Cookie 中保存了過多數據,會超出 cookie 的大小限制,這時我們可以更改 session 的保存方式,比如使用 redis,memcached 等。
~~~
Rails.application.config.session_store :redis_store, servers: {
host: "127.0.0.1",
port: 6379,
namespace: "store_session"}
~~~
在 [6.2 緩存](#) 中有其他詳細的介紹。
### 5.1.7 etag
Controller 響應的時候,header 中會包含 etag 屬性,根據這個屬性,瀏覽器會判斷該內容是否修改。
~~~
headers['ETag'] = Digest::MD5.hexdigest(body)
~~~
但對 Rails 的布局和模板而言,經常包含變動的內容,比如登錄后會顯示用戶名稱,未登錄顯示登錄連接。 并且,body 可能會很大,md5 時間長。
我們可以針對資源,單獨增加 etag:
~~~
def show
fresh_when([@product, current_user.try(:id)])
end
~~~
也可以將它精簡:
~~~
class ProductsController < ApplicationController
etag { current_user.try(:id) }
...
def show
fresh_when(@product)
end
~~~
如果我們僅提供數據,比如 api,[可以去掉模板](http://api.rubyonrails.org/classes/ActionController/ConditionalGet.html#method-i-fresh_when):
~~~
fresh_when @product, template: false
~~~
### 5.1.8 csrf
在 Controller 接收請求數據的時候,安全機制會處理跨站請求偽造(cross-site request forgery,簡稱 CSRF)。在我們的布局(layout)頁面,你可能已經看到這樣一個輔助方法:
~~~
<%= csrf_meta_tags %>
~~~
打開頁面的源碼,我們可以看到:
~~~
<meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="O3Li25wJK0buXKRQRX4CzpAWheQIQ4VknCPe3KwNIFkIuUsbBApxl2jVVTd9IcmzR8oHLZI0qZpO39aLdNaBAQ==" />
~~~
當我使用表單的輔助方法 `form_for` 和 `form_tag` 時,表單會自動創建一個隱藏控件
~~~
<input type="hidden" name="authenticity_token" value="GI5YwKDhQA4pMlLRaUlpHugYdL5ygNe3Co6TL8PvZDsrRfEAOOIa36+7o7ZRFqJjP8T2d+j3+0nYcpt4GzTFYw==">
~~~
當我們使用 `remote: true` 時,這個控件又消失了,這樣是不是不安全?不,ujs 在提交的時候,為我們自動補充上了 `authenticity_token` 參數。
更多 Rails 安全問題,可以參考這里 [http://guides.ruby-china.org/security.html](http://guides.ruby-china.org/security.html)。
注:
感謝 [Rails 4 - Zombie Outlaws](https://www.codeschool.com/courses/rails-4-zombie-outlaws),本節 3,5 的內容靈感來自。
- 寫在前面
- 第一章 Ruby on Rails 概述
- Ruby on Rails 開發環境介紹
- Rails 文件簡介
- 用戶界面(UI)設計
- 第二章 Rails 中的資源
- 應用 scaffold 命令創建資源
- REST 架構
- 深入路由(routes)
- 第三章 Rails 中的視圖
- 布局和輔助方法
- 表單
- 視圖中的 AJAX 交互
- 模板引擎的使用
- 第四章 Rails 中的模型
- 模型的基礎操作
- 深入模型查詢
- 模型中的關聯關系
- 模型中的校驗
- 模型中的回調
- 第五章 Rails 中的控制器
- 控制器中的方法
- 控制器中的邏輯
- 第六章 Rails 的配置及部署
- Assets 管理
- 緩存及緩存服務
- 異步任務及郵件發送
- I18n
- 生產環境部署
- 常用 Gem
- 寫在后面