原文:http://guides.rubyonrails.org/action_controller_overview.html
在此篇中你将学习到控制器的工作原理以及如何请求的周期内进行逻辑处理的。
通过此篇,你将了解到:
1 控制器是做什么的
ActionController是MVC架构中的C。在路由决定分配给哪个控制器来处理请求后,你的控制器主要负责接收请求并生成合适的结果。幸运的,Action Controller已经做了许多后台工作,使得你能够尽可能的使用简单。
对于大多数方便的RESTful资源型应用,控制器将从需求中检索需要的请求(这对于你作为开发者是不可见的),从数据模型层取得数据并使用合适的视图模板进行渲染输出。若你的控制器需要处理一些特殊的逻辑,这将不成问题,同时这也是应用开发中常遇到的问题。
控制器可以认为是数据模型层与视图层之间的中间者。它使得数据模型层中的数据可以显示在视图中,并且将视图中的数据保存或更新到数据模型层。
注:详情请查看Rails Routing from the Outside In章节。
2 控制器的命名约束
Rails中控制器的命名约束支持控制器名称中最后单词的复数形式,尽管它不是必须的(例如.ApplicationController)。例如,ClientsController比ClientController更好,SiteAdminsController比SiteAdminController和SitesAdminsController更好,等等。
使用默认的命名约束,你将可以直接使用生成的默认路由(例如resources等),不需要去逐个添加:path或:controller,并且可以保持URL和path帮助函数的用法在应用中是一致的。
注:控制器的命名约束不同于数据模型层的命名约束,后者默认以单数命名。
3 方法和动作
控制器是一个继承于ApplicationController的Ruby类,拥有与其他类相同的方法。当你的应用接收到需求,路由机制将决定哪个控制器和动作来执行,然后Rails将创建控制器的实例并运行与动作同名的方法。
class ClientsController < ApplicationControllerdef newendend
在例子中,若用户方法应用中的/clients/new来添加新客户,Rails将创建ClientsController的一个实例并运行new方法。注意如例子中的方法是空的,将仍然继续执行,这是因为Rails中默认渲染new.html.erb视图除非动作逻辑明确设置为其他。在new方法中创建一个Clinet新的实例变量@client,将可以在视图中使用:
def new
@client = Client.new
end
更多详情请看Layouts& Rendering Guide。
ApplicationController继承于ActionController::Base,后者中已经定义了一系列的帮助方法。此篇将概述一些此类方法,但若你想了解更多,请查看官方API文档。
只有公共的方法能够以动作形式被调用。在实际中的最佳实践是尽量降低辅助方法或过滤方法的访问等级。
4 参数
你将可能需要在控制器动作中处理从用户那里发送过来的数据或其他参数。在web应用中有两种类型的参数。第一种类型是通过URL传入的查找参数,查找字符串是URL中’?’符号之后的部分。第二种类型的参数是指通过POST传输的参数。此类信息通常来源于用户提交填充的表单。它之所以称为POST数据是因为它是作为HTTP POST请求的一部分进行传输的。Rails对于这两类参数并不进行区分,都是哈希形式存储在你控制器中的params哈希变量。
class ClientsController
4.1 哈希和数据参数
对于params哈希并不限于一维的键值对。它可以包含数据和内嵌的哈希数据。若要发送数组形式的数据,需要在键名称的后面添加”[]”符号:
GET /clients?ids[]=1&ids[]=2&ids[]=3
注:此例子中的URL实际中将被编码成“/clients?ids%5b%5d=1&ids%5b%5d=2&ids%5b%5d=3”,这是由于“[”和“]”符号在URL中是不被允许的。绝大多数情况下你不需要担心此操作,因为浏览器将会自动进行编码,当Rails接收到此类URL后也将进行解码操作,但是你若要在URL中传入此类符号时,你需要注意此种情况。
通过params[:ids]取得的值为[“1”,”2”,”3”]。注意参数的值是字符串形式;Rails中不对参数的类型进行任何转换。
注:默认情况下Rails考虑到安全会将params参数中的[],[nil],[nil,nil,…]此类参数的值转换成nil。详情请查看SecurityGuide获取更多信息。
通过将client[name]形式将name数据包装成哈希值进行传输:
当此表单被提交的时候,params[:client]的值将是 { "name" => "Acme", "phone"=> "12345", "address" => { "postcode" =>"12345", "city" => "Carrot City" } }。注意其中的params[:client][:address]是嵌套哈希。
注意params哈希参数实际上是AciveSupport::HashWithIndifferentAccess类的实例对象,通过此类可以无差别的操作符号和字符串两种形式主键的哈希数据。
4.2 JSON数据
若你正在写web应用,你可能会发现使用JSON数据来进行交互更方便。若你的HTTP请求头部的“Content-Type”设置为“application/json”,那么Rails将自动将你的参数信息转换成JSON格式并放入params哈希中,通过params可以方便的访问到想要的信息。
例如,若你正在发送以下JSON格式的内容:
{ "company": { "name": "acme","address": "123 Carrot Street" } }
你从params[:company]中得到的数据是{"name" => "acme", "address" => "123Carrot Street" }。
若你在初始配置中打开了config.wrap_parameters或在控制器中调用了wrap_parameters,你可以放心的省略掉JSON格式中的头结点元素。Rails将依据传入的控制器名称对传入的参数进行克隆和包裹。因此,以上的参数形式可以写为:
{ "name": "acme","address": "123 Carrot Street" }
假如你将数据发送至CompaniesController中,它将被:company键包裹:
{ name:"acme", address: "123 Carrot Street", company: { name:"acme", address: "123 Carrot Street" } }
你能够自定义键名或你想包裹的具体参数,详情请查看APIdocument。
注:对于XML格式参数的支持已经封装到actionpack-xml_parser gem包。
4.3 路由参数
虽然params哈希中总是包含:controller和:action键值对,但是你应该使用controller_name和action_name方法来获取这些值。其他通过路由定义的参数,如:id也同样可访问。例如,显示一个列表,该列表用于显示激活或非激活状态的客户。我们可以添加一个包含有:status参数的路由:
get '/clients/:status'=> 'clients#index', foo: 'bar'
在此例中,当用户打开一个URL如/clients/active,params[:status]将被设置为active。当此路由被使用后,params[:foo]将被设为‘bar’,与传入查询参数类似。同理,params[:action]被设置为“index”。
4.4 default_url_options
你可以通过在控制器中定义default_url_options方法来定义全局的路由默认参数。该方法必须返回哈希类型的值,并且必须是符号形式的键值对。
class ApplicationController
这些参数将用于生成URL,但若使用url_for方法传入的参数将可能会覆盖默认的参数。
若你在ApplicationController中定义了default_url_for,如下所述,它将用于所有生成的URL中。该方法同样也可以定义在单个控制器中,这样它只作用于指定控制器生成的URL。
4.5 强参数
使用强参数的功能,主要目的是用于ActionController中阻止大量赋值的参数作用于Active Model,除非这些参数被加入白名单。这将意味着你必须做出明确选择,那些参数被允许大量赋值或更新,那些参数不被允许。
另外,参数可以被标记为required并且可通过前置的raise/rescue机制捕获异常信息返回400请求失败页面。
class PeopleController < ActionController::Base #此方法将触发ActiveModel::ForbiddenAttributes 异常因为使用了大量赋值,没有对参数进行加入白名单操作。 def create Person.create(params[:person]) end # 此方法中虽然有person_params设定参数白名单,但仍然会触发ActionController::ParameterMissing异常,此异常将被ActionController::Base捕获,并返回400请求失败页面。 def update person = current_account.people.find(params[:id]) person.update!(person_params) redirect_to person end private # 通过私有方法来封装添加到白名单的参数列表,便于在多个方法中共用。当然,你也可以在此方法中添加权限控制。 def person_params params.require(:person).permit(:name, :age) end end
4.5.1 允许标量值
若有:params.permit(:id)
若params中有:id键并且有被允许的标量值关联,那么:id键将被加入到白名单。否则该键将被过滤掉,因此数组、哈希或其他对象都不能够注入。
允许的标量类型有String, Symbol, NilClass, Numeric, TrueClass, FalseClass, Date, Time, DateTime, StringIO, IO, ActionDispatch::Http::UploadedFile和Rack::Test::UploadedFile。
若要声明params中的值必须是可接受的数组类型,可如下写:
params.permit(id: [])
若要将整个哈希结构添加到白名单,可如此写:
params.require(:log_entry).permit!
这将允许:log_entry的哈希结构数据并且其结构内的所有子哈希结构数据。使用permit!方法时,注意此方法允许所有的当前和未来的数据模型属性都可被大量赋值。
4.5.2 嵌套参数
你可以设置允许的嵌套参数:
params.permit(:name, {emails: [] },
friends: [:name, { family: [ :name ], hobbies: [] }])
例子中声明的白名单包括name,emails和friends属性。此例子中允许接收数组类型的emails参数(数值在允许的标量值范围内)和有name(任何被允许的标量值)、hobbies(数组类型的标量值)、family(被允许带有name属性,该name属性可为任一允许的标量值)属性的friends属性。
4.5.3 更多例子
你若想在new动作中使用白名单参数。你不能使用在root键上使用require方法,因为在new动作中root对象还不存在。
# 使用fetch方法,能够设置默认的并且是强类型参数的API。
params.fetch(:blog, {}).permit(:title, :author)
accepts_nested_attributes_for 允许你更新或删除关联的记录。此方法基于id和_destroy参数:
# permit :id and:_destroy
params.require(:author).permit(:name, books_attributes: [:title,:id, :_destroy])
键为整数的哈希结构的处理方式不同其他,若此类哈希结构中,一些属性有直接的孩子节点,那么可以直接声明该属性。如有has_many的关联关系,并使用accepts_nested_attributes_for方法,可以获得如下数据:
# 将下列数据添加到白名单:
# {"book" => {"title" => "SomeBook",
# "chapters_attributes" => { "1" => {"title"=> "First Chapter"},
# "2" => {"title" => "Second Chapter"}}}}
params.require(:book).permit(:title, chapters_attributes: [:title])
4.5.4 强参数的范围之外
强参数API适用于通常绝大多数使用环境。但并不意味着能够解决你所有的白名单问题。你可以根据实际情况简单的设置你的API。
假设有这样一个场景,你有产品名称和产品关联的哈希数据,你想将产品名称属性与关联的数据添加到白名单中。这在强参数API中,是不允许直接将内嵌的哈希数据加入到白名单中,但是你能够使用内嵌哈希的键值来声明此属性为允许。
def product_params
params.require(:product).permit(:name,data: params[:product][:data].try(:keys))
end
5 会话
你应用中对于每个用户都有一个会话,该会话可用于存储小量需要持久在请求之间的数据。会话只能够用于控制器和视图中,并且可以使用下列任一个会话存储机制:
所有的会话使用cookie来存储每个会话的唯一ID(你必须使用cookie,由于安全原因,Rails将不允许你在URL中传入会话id)。
大多数的存储,存储的ID主要要入用于在服务端查找会话数据,例如查找数据库中的数据。有一个例外,在使用默认推荐的会话存储机制——CookieStore,在此机制中将所有会话数据存储在cookie中(若你需要,存储的ID仍可以使用)。这是非常方便的、轻量级的并且在新的应用中可直接操作会话,而不需要额外的操作。由于会话信息是经过加密的,其他人无法读取器数据。(若会话信息被编辑,Rails不接收该数据)
CookieStore机制能够存储大约4kB的数据——比其他存储相对较小,但是这足够存储常用信息。在会话中存储大量数据时不被推荐的,无论采用任何的存储机制。你应该在会话中避免存储过于复杂的对象(除了基本的Ruby对象,常用的数据模型实例),由于服务器端将不能够在请求之间重组他们,因此将会报错。
若你在用户会话中不需要存储敏感数据或不需要维持过场时间(例如若你仅仅存储flash和message信息),你可以考虑使用ActionDispatch::Session::CacheStore机制。这将使用你在应用中配置的缓存来存储会话数据。这样做的好处是你不需要额外的步骤或管理这些数据,直接使用现存的Rails缓存机制来存储。
了解更多详情请查看Security Guide.
若你需要使用不同会话存储机制,可配置config/initializers/session_store.rb文件:
# 使用数据库存储机制来替代默认的Cookie存储机制,在存储中不要存储过于敏感的数据。(创建会话数据表使用rails g active_record:session_migration)
# YourApp::Application.config.session_store :active_record_store
Rails中当存储会话数据时,设置了会话key(即cookie的名字)。可通过config/initializers/session_store.rb来修改:
# 当你修改此文件后,确保重启服务
YourApp::Application.config.session_store :cookie_store, key:'_your_app_session'
你也可以声明:domain键值并在cookie中使用域名的名字:
# 当你修改此文件后,确保重启服务
YourApp::Application.config.session_store:cookie_store, key: '_your_app_session’, domain: “.example.com”
Rails设置一个秘钥键(CookieStore机制中)要与会话数据签名。这也可以通过配置config/initializers/secret_token.rb来改变:
# 当你修改此文件后,确保重启服务
#你的秘钥键值主要用于验证签名cookie的数据完整性。
# 若你修改了此键值,所有的已签名的cookie将不再有效。
# 请确保该秘钥至少有30个字符并且具有随机性不包含常用的单词,否则你将暴露在字典攻击下。
YourApp::Application.config.secret_key_base ='49d3f3de9ed86c74b94ad6bd0...'
注:使用CookieStore机制,当改变秘钥后,所有的已存在的cookie都将变得无效。
5.1 访问会话
在你的控制器中你可以通过session实例方法来访问会话信息。
注:会话是懒惰加载的。若你不需要访问会话中的信息,它们将不进行加载。因此你将不需要去取消会话,只要不访问它们即可。
会话使用键值对的形式存储数据(与哈希结构相似):
class ApplicationController < ActionController::Base
private
# 查找会话中用户ID是否存在,此ID是通过:current_user_id键进行设置会话的。这种做法是Rails应用中普遍的存储用户登录信息方式;登录成功后设置会话键值对,退出的时候移除此键值对。
def current_user
@_current_user ||= session[:current_user_id] &&
User.find_by(id: session[:current_user_id])
end
end
在会话中存储一些信息,主要做的只是像哈希一样赋值即可:
class LoginsController < ApplicationController
#"Create" a login, aka "log the user in"
def create
if user = User.authenticate(params[:username], params[:password])
# 保存用户ID于会话中,以便在后续使用它。
session[:current_user_id] = user.id
redirect_to root_url
end
end
end
从会话中删除一些信息,只需要将对应键的值设置为nil即可:
class LoginsController < ApplicationController
#"Delete" a login, aka "log the user out"
def destroy
# 从会话中删除用户ID信息。
@_current_user = session[:current_user_id] = nil
redirect_to root_url
end
end
若要重置所有的会话信息,使用reset_session。
5.2 Flash
Flash类型的会话是比较特别的存在,它会在每次请求进行清空。这将意味着存在在其中的数据只能在下一次请求中访问,常用于显示错误信息。
Flash数据的访问与会话相同,也是哈希似的操作。(实际上Flash是FlashHash的实例方法)
以用户退出系统为例子,控制器需要能够发送消息,在下个请求来提醒用户已经安全退出:
class LoginsController< ApplicationController
def destroy
session[:current_user_id] = nil
flash[:notice] = "You have successfully logged out."
redirect_to root_url
end
end
注意例子中复制了flash消息,此消息将作为重定向操作的一部分。你能够使用:notice,:alert或常用的:flash。
redirect_toroot_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方法将重定向到应用的root_url即根路径,在此页面进行显示消息。注意在重定向后能够显示Flash类型的消息,但是具体要做什么完全取决于下一个动作。通过flash消息来显示错误警告或提示信息,并使用应用中的布局显示将非常方便。
<% flash.each do |name, msg| -%>
<%= content_tag :div, msg, class: name %>
<% end -%>
若如此设置,在应用中设置了提示信息或警告信息,此布局将自动显示此消息。
你能够传入flash任何会话中可存储的对象;你能不受限的去提醒和警告:
<% ifflash[:just_signed_up] %>
Welcome to our site!
<% end %>
若你想要flash的值保留到下一个请求,可以使用keep方法:
class MainController < ApplicationController
# 此动作对应的是root_url路径,但是你想要所有发送到此路径的请求都重定向到UsersController#index。若一个动作设置了flash消息并重定向到root_url,那么flash的值将在下一次重定向的时候清空,但你可以使用keep方法来保留消息到下一个请求。
defindex
# 将保持所有的flash消息
flash.keep
# 你也可以通过以下形式保留特定的消息
# flash.keep(:notice)
redirect_to users_url
end
end
5.2.1 flash.now
默认情况下,设置的flash只能在下一次请求中访问,但实际中你可能需要在同一次请求中访问请求。例如,若create(即创建动作)动作执行失败,返会到new模板界面,这将不会产生新的请求,但你仍需要显示一些错误信息。在此种情况中,你可以使用flash.now来实现,其使用方法与flash相同:
class ClientsController < ApplicationController
def create
@client = Client.new(params[:client])
# ...
else
flash.now[:error] = "Could not save client"
render action: "new"
end
end
end
6 Cookie
你应用能够存储少量数据到客户端——此类数据叫做cookie——此类数据可以在不同请求甚至会话间保留。Rails中提供了cookie方法,通过此方法可以方便的访问cookie信息,就像方法会话一样,其访问方法与哈希结构类似:
class CommentsController < ApplicationController def new # 如果评论者的名字已存储在cookie中,进行自动填充。 @comment = Comment.new(author: cookies[:commenter_name]) end def create @comment = Comment.new(params[:comment]) if @comment.save flash[:notice] = "Thanks for your comment!" if params[:remember_name] # 记录评论者的名字 cookies[:commenter_name] = @comment.author else # 删除cookie中存储的评论者的名字信息 cookies.delete(:commenter_name) end redirect_to @comment.article else render action: "new" end end end
注意在会话中删除会话信息是将其设置为nil,而在cookie中是使用delete方法操作,如:cookie.delete(:key)。
Rails也提供了对cookie的加密机制和签名机制,来保护存储的敏感数据。签名机制将在cookie值后追加密码签名来保护数据的完整性。加密机制将对cookie值进行加密并进行签名标记,以便终端用户无法查看信息。更多细节请查看APIdocumentation。
这些特殊的cookie机制是将值进行序列化,并在Ruby对象中读取时反序列化。
你可以配置使用何种类型的序列化:
Rails.application.config.action_dispatch.cookies_serializer= :json
对于Rails项目默认情况下使用:json序列化机制。为了兼容旧应用中已存在的cookie,若serializer选项未被声明将使用:marshal机制。
你可以设置参数选项为:hybrid,Rails将会将已存在的cookie(Marshal序列化)反序列化读取出来再以JSON格式写入。此选项在将应用迁移至:json序列化机制时非常有用。
也可以设置自定义的序列化机制:
Rails.application.config.action_dispatch.cookies_serializer = MyCustomSerializer
当使用:json或:hybrid序列化的时候,你需要注意不是所有的Ruby对象都可以序列化成json格式。例如,对于Date和Time对象将序列化成字符串,并且Hash对象有自己的存储键字符串化方法。
class CookiesController < ApplicationController
def set_cookie
cookies.encrypted[:expiration_date] = Date.tomorrow # => Thu, 20 Mar2014
redirect_to action: 'read_cookie'
end
def read_cookie
cookies.encrypted[:expiration_date] # => "2014-03-20"
end
end
建议你只存储简单的数据(字符串和数字)到cookie中。若你必须存储复杂的对象,你必须手动处理对后续请求数据的读取。
若你使用cookie会话存储,此类特性也适用于session和falsh哈希结构。
7 渲染XML和JSON数据
ActionController是的生成XML或JSON格式数据非常容易。若你使用脚手架功能生成了控制器,那么你会看到如下类似的代码:
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 过滤器
过滤器是一些方法,这些方法运行在控制器动作之前/之后/前后都有。
过滤器可以被继承,因此你可以在ApplicationController中设置,将运行于所有的控制器。
前置过滤器可能终止请求。常用的一个前置过滤器就是当运行控制器动作时判断用户是否已登录。如下:
class ApplicationController < ActionController::Base
before_action:require_login
private
def require_login
unless logged_in?
flash[:error] = "You must be logged in to access thissection"
redirect_to new_login_url # halts request cycle
end
end
end
若用户没有登录,例子中该方法将返回错误消息并定向到登录界面。若前置过滤器进行了渲染或重定向页面操作,那么原来要访问的动作将不再执行。若在此过滤器后仍有多个过滤器,这些过滤器也将不予执行。
例子中,将过滤器添加到了ApplicationController中,因此应用中所有的控制器都继承了该过滤器。这将使得应用的所有功能都需要用户登录后才能使用。实际中常常有些功能的访问不需要用户进行登录就可查看,即并非所有的控制器动作都需要登录操作。你可以通过使用skip_before_action操作来添加例外的动作:
class LoginsController < ApplicationController
skip_before_action:require_login, only: [:new, :create]
end
现在,例子中LoginsController的new和create动作将不需要用户登录即可操作。例子中的:only选项常用于设置跳过过滤器的具体动作,还有另一个选项:except以同种方式工作。这些选项也用于添加过滤器的时候设置有效的动作对象。
8.1 后置过滤器和前后置过滤器
除了前置过滤器,你也能够在控制器动作之后或前后都有进行执行过滤器。
后置过滤器与前置过滤器相似,但是由于在执行过滤器之前动作已经执行将返回信息返回到客户端,因此将无法终止动作的运行。
前后置过滤器主要是通过yield来运行它们关联的动作,与Rack中间件工作相似。
例如,在一个网站中,管理员需要有批准流程来批准哪些修改是可用的,此功能可以通过一个事物来实现:
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
注意前后置的过滤器也包裹渲染。特别的,在例子中视图将从数据库中读取信息(例如通过scope),这些将在事物中处理后再显示预览数据。
你能够选择不去yield,自行构建返回的信息,此种情况下动作将不再执行。
8.2 使用过滤器的其他途径
通常使用过滤器大多是创建私有方法然后通过*_action方法来添加它们,还有另外两种方式做同样的事情。
第一种方式是直接传入块参数给*_action方法。块参数中接收控制器作为参数,上面例子中的require_login过滤方法可以写成:
class ApplicationController < ActionController::Base
before_actiondo |controller|
unless controller.send(:logged_in?)
flash[:error] = "You must be logged in to access thissection"
redirect_to new_login_url
end
end
end
注意到此例中使用了send,是因为logged_id?方法是一个私有方法并且过滤器不在控制器范围内运行。此种方式不推荐,但是在简单的用例中非常有效。
另一种方式是使用类(实际上,任何能够对正确方法响应的对象就可)来控制过滤器。这种方式常用于有许多复杂的、不能够通过其他两种方式以易读和可重用的形式实现的情况。作为例子,你能够通过类来重写登录过滤器:
class ApplicationController < ActionController::Base
before_actionLoginFilter
end
class LoginFilter
def self.before(controller)
unless controller.send(:logged_in?)
controller.flash[:error] = "You must be logged in to accessthis section"
controller.redirect_to controller.new_login_url
end
end
end
此例子并不是个理想的例子,因为它将不运行在控制器范围内,但是需要控制器作为参数传入。过滤器类中必须实现一个与过滤器名称相同的方法,如对于before_action过滤器,类中必须实现一个before方法,等等。对于around方法必须使用yield来执行动作。
9 请求欺骗防范
跨站请求伪造是一种攻击,网站可通过欺骗用户来像另一个站点发送请求,可能使用用户在网站中的权限对数据进行增删改操作。
避免此类攻击的第一步是确保所有的破坏操作(对数据库的创建、更新和删除)使用非GET请求方式。若你使用了RESTful资源型约定,那么你已经做了此步。然而,一些恶意网站能够发送一些非GET请求到你的网站,这就需要使用跨站请求伪造保护。同样的,它也将阻止伪造的请求。
它的实现是为每个请求添加一个无法推测的token值,此值只被服务器与相应请求间知道。若到来的请求没有正确的token值,服务器将拒绝访问。
若你生成一个表单,如下:
<%= form_for @user do |f| %>
<%= f.text_field:username %>
<%= f.text_field:password %>
<% end %>
你将看到token是作为隐藏元素加入到表单中的:
Rails中通过表单帮助方法(即form helper)为每个表单添加token,因此多数情况下你不需要担心此类攻击。若你手动写了一个表单或因为其他原因需要添加token,可以通过form_authenticity_token方法来实现。
对于form_authenticity_token方法将生成有效的认证token。这个在Rails不能够自动添加token的情况下非常有用,如自定义的Ajax调用。
Security Guide章节中,有更多有关开发过程中应该注意的安全相关的问题详解。
10 请求和响应对象
在每个控制器中有两个可访问当前请求生命周期内的请求和响应对象的方法。对于request方法包含了一个AbstractRequest实例和response方法将返回发送给客户端的响应数据。
10.1 请求对象
请求对象中包含了许多请求发送方的相关信息。查看所有可用方法请参考API document。你可以访问请求对象的一些属性,如下:
请求属性
内容
host
存储请求方的主机地址
domain(n=2)
从右向左获取请求地址的前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
10.1.1 path_parameters,query_parameters和request_parameters
Rails收集了所有随请求发送的params哈希参数,无论是作为查询字符串的一部分还是发送内容的一部分。请求对象依据参数的来源提供了3个方法来访问。对于query_parameters哈希包含随查询字符串发送的参数;request_parameters哈希包含了在发送内容中的参数;path_parameters哈希包含了请求路径中的路由参数。
10.2 响应对象
响应对象不经常直接使用,但是在动作执行和数据渲染过程中,有时候可以通过后置过滤器进行直接访问。响应对象的一些属性有设置方法,允许你修改它们的值:
属性
内容
body
存储返回数据的字符串。通常是HTML。
status
返回的HTTP状态码,如200代表请求成功或404代表文件未发现
location
记录跳转的地址
content_type
响应的内容类型
charset
响应的字符编码,通常是‘utf-8’
headers
响应的头部数据
10.2.1 设置自定义的头部
若你想想设置自定义的响应头部,可通过response.headers来设置。头部信息是以哈希键值对形式存储,Rails将自动将设置信息转换为哈希。若你需要修改或添加头部信息,只需要设置response.header即可:
response.header[“Content-Type”] = “application/pdf”
注:在例子中使用content_type方法设置更好。
11 HTTP认证
Rails内置了两种HTTP认证机制:
11.1 HTTP普通认证机制
HTTP普通认证是一种支持绝大多数浏览器和HTTP客户端的认证策略。例如,对于未授权的部分,需要用户输入密码和用户名到HTTP普通对话框中。使用内置的认证机制是非常方便的,只需要通过访问一个方法即可,http_basic_authenticate_with。
class AdminsController < ApplicationController
http_basic_authenticate_withname: "humbaba", password: "5baa61e4"
end
对于其他控制器,只需要继承AdminsController,即可获取同样的认证策略。
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_requst_with_http_digest方法的块参数中只有一个参数——username。块将返回密码。该方法若认证成功返回true,否则返回false。
12 数据流和文件下载
有些时候你需要发送文件来替代HTML页面。在Rails中所有的控制器都有send_data和send_file方法,都可以发送数据流到客户端。send_file可以方便的将硬盘中的文件发送出去。
使用send_data发送数据:
require "prawn"
class ClientsController < ApplicationController
#Generates a PDF document with information on the client and
# returnsit. 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.newdo
text client.name, align: :center
text "Address: #{client.address}"
text "Email: #{client.email}"
end.render
end
end
例子中download_pdf动作将调用私有方法来生成pdf,该方法将返回字符串。该字符串将被以文件的形式发送到客户端,并为用户设定一个文件名。有些时候,当发送数据流到客户端时,你可能不想让用户下载文件。拿图片来说,例如,你能够将图片嵌入到页面中。可以通过设置:disposition参数为”option”来告诉浏览器文件不能够被下载。该选项的默认值为”attachment”,是允许下载的。
12.1 发送文件
若你想发送已存在于硬盘中的文件,可以使用send_file方法:
class ClientsController < ApplicationController
# 发送文件的数据流。
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
此方法将每次读取数据流中4kB信息,避免完全将文件全部加载到内存中。是可以通过:stream参数关闭数据流操作或者通过:buffer_size选项配置数据块大小。
若:type未被声明,将通过:filename中文件的后缀名进行判断。若文件的格式未被注册,application/octet-stream将被使用。
注:当使用来自客户端的请求中参数(params,cookie等)定位硬盘中的文件,将显露出安全问题,可能会有恶意请求想获取未被允许的文件。
注:在Rails中不推荐直接使用硬盘中静态文件,你可以将文件放入到服务器中public文件夹下。推荐通过Apache或其他web服务器来提供文件下载,这将使得下载请求不必经过Rails栈处理。
12.2 REST资源下载
虽然send_data方法运行的不错,但若你创建了资源型应用,通过不同的动作来操作文件下载,此方法将无需调用。在REST终端,上例子中的PDF文件能够以其他客户端资源形式表现。Rails提供了更为便捷的资源型下载方式。将上例改写为不需要流处理,使用show动作进行处理,如下:
class ClientsController < ApplicationController
# 用户可以请求接收pdf或HTML资源。
def show
@client = Client.find(params[:id])
respond_to do |format|
format.html
format.pdf { render pdf: generate_pdf(@client) }
end
end
end
为了使得此例可运行,你必须将PDFMIME格式加入到Rails中。可以通过配置config/initializers/mime_types.rb来实现:
Mime::Type.register"application/pdf", :pdf
注:配置文件后,此文件将不会被重新加载。因此你必须重启服务器,使其生效。
现在用户能够通过在URL后添加’.pdf’来请求获得PDF格式信息:
GET/clients/1.pdf
12.3 数据实时流
Rails允许你将更多的非文件类型数据进行流式处理。实际上,你能够流式处理一切响应对象中的信息。ActionController::Live模块允许你与浏览器之间创建持久的连接。使用此模块,你将能够实时发送特定的数据到浏览器。
12.3.1 实时合并流
若包含ActionController::Live在你的控制器中,你能够在所有动作中对数据进行流式处理。你能够min-in此模块:
class MyController < ActionController::Base
includeActionController::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”消息,每次发送间隔1秒。
在上例中,需要注意到数据流的打开/关闭操作。使用流时,要确保关闭流。若未调用流关闭操作,流将永远保持打开状态。我们也可以在向响应流中写入数据前,设置内容类型为text/event-stream。这是因为在write或commit响应对象时,头部信息不能够在响应信息已被提交后(当使用response.committed返回true时提交)修改。
12.3.2 用例
假设你将创建卡拉OK应用,用户通过该应用可以获得具体歌曲的歌词。歌词中有行编号和每行的执行时间(可通过num_beats方法获得)。
若你想返回kalaok应用中的歌词(只有歌词的前一行唱完后,才能显示下一行歌词),可以通过使用ActionController::Live:
class LyricsController < ActionController::Base
includeActionController::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 注意事项
对数据进行流式处理是非常有用的工具。诸如上面的例子中,你能够选择在什么时候、发送什么信息进行响应。然而你应该注意以下几点:
13 日志过滤器
Rails在log文件夹下记录了每个环境对应的日志。在调试应用时,日志文件将变得非常有用,但是在实时的应用中你可能不希望,将信息的每小块都存储到日志文件中。
13.1 参数过滤
通过应用的配置文件设置config.filter_parameters参数,可以在日志文件中对设置的敏感参数进行过滤。这些过滤的参数,将在日志文件中显示为[FILTERED]。
config.filter_parameters << :password
13.2 重定向过滤
某些时候,你可能需要在日志文件中过滤掉一些敏感定向地址。可以通过设置config.filter_redirect配置实现此功能。
config.filter_redirect<< 's3.amazonaws.com'
你能够将其设置为字符串、正则表达式或者数组:
config.filter_redirect.concat ['s3.amazonaws.com', /private_path/]
匹配的URLs将被标记为[FILTERED]。
14 异常页面
许多情况下,你的应用将需要对程序的bug或异常进行处理。例如,若用户点击了已无效的链接,Active Record将返回ActiveRecord::RecordNotFound异常。
Rails默认的异常处理会显示“500Server Eroor”信息。若请求是本地的,将添加一些追踪信息或其他相关信息,这些信息有助于问题修复。若请求是Rails的remote方式(Ajax方式),只显示简单的500消息页面,或当路由信息错误、信息已不存在,则返回404错误页面。有时候你能够自定义如何处理异常和显示什么消息给用户。在Rails应用中,有几个异常捕获等级。
14.1 默认的500和404模板
在生产环境下,默认将渲染404或500错误消息。这些错误消息被分别包含在public文件夹中的404.html和500.html页面中。你能够自定义这些文件并添加一些额外信息和布局,但是确保这些写信是静态的,不能使用RHTML或应用中的布局只能是HTML。
14.2 rescue_from
若你想在捕获错误时做更多处理,可以使用rescue_froml来捕获控制器中或子类控制器中的特定异常或多个异常。
当一个异常被rescue_from指令捕获,异常对象将传入到收获句柄。此句柄可以是一个方法或使用:with参数传入的Proc对象。你能够直接使用块来替代Proc对象。
此例子中通过rescue_from捕获所有的ActiveRecord::RecordNotFound错误并处理它们:
class ApplicationController < ActionController::Base
rescue_fromActiveRecord::RecordNotFound, with: :record_not_found
private
def record_not_found
render plain: "404 Not Found", status: 404
end
end
显然,此例子中知识捕获了所有的指定异常来进行自定义处理,并未改变默认的异常处理。例如,你能够创建自定义异常类,当未认证用户进行无权限访问时抛出此类异常:
class ApplicationController < ActionController::Base
rescue_fromUser::NotAuthorized, with: :user_not_authorized
private
def user_not_authorized
flash[:error] = "You don't have access to this section."
redirect_to :back
end
end
class ClientsController < ApplicationController
# 检测用户是否有权限访问
before_action:check_authorization
# 注意所有的动作为什么不需要担心权限问题
def edit
@client = Client.find(params[:id])
end
private
# 若用户未认证,抛出异常
def check_authorization
raise User::NotAuthorized unless current_user.admin?
end
end
注:主要的异常只能通过ApplicationController类进行捕获,因为这些异常会在控制器初始化和动作执行前进行抛出。更多信息请查看PratikNaik的文章。
15 强制使用HTTPS协议
某些情况下出于安全考虑,你可能强制特定的控制器只能通过HTTPS协议进行访问。你可以通过force_ssl方法进行设置:
class DinnerController
force_ssl
end
就像过滤器,你可以使用:only和:except选项来设定安全连接针对的具体动作:
请注意若你发现自己在多数控制器中设置了force_ssl,你也许通过强制整个应用使用HTTPs协议替代更好。此种情况下,可以通过环境配置文件设置config.force_ssl实现。class DinnerController
force_sslonly: :cheeseburger
# or
force_sslexcept: :cheeseburger
end