CSRF(Cross-Site Request Forgery) 是一种常见的攻击手段,Rails 中下面的代码帮助我们的应用来阻止 CSRF 攻击。
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
end
这段代码是 Rails4 自动生成的,这里使用了with: :exception
设置了对在handle_unverified_request
使用的策略是抛出异常ActionController::InvalidAuthenticityToken
。Rails3 中默认使用的reset_session
。
Rails 防止 CSRF 的机制是在表单中随机生成一个 authenticity_token,同时存储于表单的隐藏域以及当前的 session 中,当表单提交时,而 server 端就可以比较这两处的是否一致来做出判断,判断请求的来源是否可靠,因为第三方是无法知道 session 中的 token 的。
# Sets the token value for the current session.
def form_authenticity_token
session[:_csrf_token] ||= SecureRandom.base64(32)
end
<div style="margin:0;padding:0;display:inline">
<input name="utf8" type="hidden" value="✓">
<input name="authenticity_token" type="hidden" value="EZWDs44j5vzY+DCsgTHL0iPYiOUwaFnemwtGmo2AVRM=">
</div>
当然,这些都是正常情况,当表单要作为 ajax 提交,也就是data-remote=true
时,情况就不同了,默认配置下,authenticityt_token
不再自动生成。如果是 Rails3 就会发现 session 中的信息不见了,如果是把 user_id 存储在 session 中的,当然登录的状态就改变了。如果是 Rails4,默认就会得到上面提到的InvalidAuthenticityToken
异常。
#form_tag_helper.rb
def html_options_for_form(url_for_options, options)
options.stringify_keys.tap do |html_options|
...
if html_options["data-remote"] &&
!embed_authenticity_token_in_remote_forms &&
html_options["authenticity_token"].blank?
# The authenticity token is taken from the meta tag in this case
html_options["authenticity_token"] = false
elsif html_options["authenticity_token"] == true
# Include the default authenticity_token, which is only generated when its set to nil,
# but we needed the true value to override the default of no authenticity_token on data-remote.
html_options["authenticity_token"] = nil
end
end
end
上面代码的 5-14 行可以看到生成 token 时的配置判断,从中也可以得到解决的两种办法: 1. 配置
config.action_view.embed_authenticity_token_in_remote_forms = true
2. 通过 JS 获取
其实在默认的 layout 中,一般会有一行<%= csrf_meta_tags %>
,它的定义是:
def csrf_meta_tags
if protect_against_forgery?
[
tag('meta', :name => 'csrf-param', :content => request_forgery_protection_token),
tag('meta', :name => 'csrf-token', :content => form_authenticity_token)
].join("\n").html_safe
end
end
它在页面的 head 中增加一个csrf-token
的属性
meta content="authenticity_token" name="csrf-param" />
meta content="VY13wlC2rgGccbkxyvm7Z1WX4LKH+71vzIj+8Um0QO8=" name="csrf-token" />
这与表单渲染出的 authenticity_token 完全一致,所以这就给了我们通过 js 来给表单设置 authenticity_token 的办法,如下
//application.js
$('input[name=authenticity_token]').val($('meta[name=csrf-token]').attr('content'))