Rails,设计认证,CSRF 问题
Posted
技术标签:
【中文标题】Rails,设计认证,CSRF 问题【英文标题】:Rails, Devise authentication, CSRF issue 【发布时间】:2012-08-04 10:34:18 【问题描述】:我正在使用 Rails 做一个单页应用程序。登录和注销时,使用 ajax 调用 Devise 控制器。我遇到的问题是,当我 1)登录 2)注销然后再次登录不起作用。
我认为这与我退出时重置的 CSRF 令牌有关(尽管它不应该 afaik),并且由于它是单页,旧的 CSRF 令牌正在 xhr 请求中发送,从而重置会话。
更具体地说,这是工作流程:
-
登录
退出
登录(成功 201。但是在服务器日志中打印
WARNING: Can't verify CSRF token authenticity
)
后续 ajax 请求失败 401 unauthorized
刷新网站(此时页眉中的CSRF变为其他内容)
我可以登录,它可以正常工作,直到我尝试退出并重新登录。
非常感谢任何线索!让我知道是否可以添加更多详细信息。
【问题讨论】:
请问您能否回答这个非常相似的问题? ***.com/questions/50159847/… 【参考方案1】:检查您是否已将其包含在您的 application.js 文件中
//= 需要 jquery
//= 需要 jquery_ujs
原因是 jquery-rails gem 默认情况下会在所有 Ajax 请求上自动设置 CSRF 令牌,需要这两个
【讨论】:
是的,我有这些。我用开发工具检查了请求,它们都有 CSRF。 检查你得到的是相同的 CSRF 令牌还是不同的令牌【参考方案2】:我也遇到了这个问题。这里发生了很多事情。
TL;DR - 失败的原因是 CSRF 令牌与您的服务器会话相关联(无论您是登录还是注销,您都有一个服务器会话)。 CSRF 令牌包含在每次页面加载时页面的 DOM 中。注销时,您的会话被重置并且没有 csrf 令牌。通常,注销会重定向到不同的页面/操作,这会为您提供一个新的 CSRF 令牌,但由于您使用的是 ajax,因此您需要手动执行此操作。
您需要重写 Devise SessionController::destroy 方法以返回您的新 CSRF 令牌。 然后在客户端,您需要为注销 XMLHttpRequest 设置成功处理程序。在该处理程序中,您需要从响应中获取这个新的 CSRF 令牌并将其设置在您的 dom 中:$('meta[name="csrf-token"]').attr('content', <NEW_CSRF_TOKEN>)
更详细的说明您很可能在 ApplicationController.rb 文件中设置了protect_from_forgery
,所有其他控制器都从该文件继承(我认为这很常见)。 protect_from_forgery
对所有非 GET html/javascript 请求执行 CSRF 检查。由于 Devise Login 是一个 POST,它执行一个 CSRF 检查。如果 CSRF 检查失败,则清除用户的当前会话,即将用户注销,因为服务器认为这是一次攻击(这是正确/期望的行为)。
因此,假设您在注销状态下开始,您会重新加载页面,并且永远不会再次重新加载页面:
在呈现页面时:服务器将与您的服务器会话关联的 CSRF 令牌插入页面。您可以通过在浏览器$('meta[name="csrf-token"]').attr('content')
的 JavaScript 控制台中运行以下命令来查看此令牌。
然后您通过 XMLHttpRequest 登录: 此时您的 CSRF 令牌保持不变,因此您的 Session 中的 CSRF 令牌仍然与插入到页面中的相匹配。在后台,在客户端,jquery-ujs 正在侦听 xhr 并自动为您设置一个值为 $('meta[name="csrf-token"]').attr('content')
的“X-CSRF-Token”标头(请记住,这是步骤 1 中由断绝)。服务器将 jquery-ujs 在标头中设置的 Token 与存储在会话信息中的 Token 进行比较,它们匹配,因此请求成功。
然后您通过 XMLHttpRequest 注销: 这会重置会话,为您提供一个没有 CSRF 令牌的新会话。
然后您通过 XMLHttpRequest 再次登录: jquery-ujs 从 $('meta[name="csrf-token"]').attr('content')
的值中提取 CSRF 令牌。该值仍然是您的 OLD CSRF 令牌。它使用这个旧令牌并使用它来设置“X-CSRF-Token”。服务器将此标头值与它添加到会话中的新 CSRF 令牌进行比较,这是不同的。这种差异会导致protect_form_forgery
失败,从而引发WARNING: Can't verify CSRF token authenticity
并重置您的会话,从而将用户注销。
然后您发出另一个需要登录用户的 XMLHttpRequest:当前会话没有登录用户,因此设计返回 401。
更新:8/14 设计注销不会给你一个新的 CSRF 令牌,通常在注销后发生的重定向会给你一个新的 CSRF 令牌。
【讨论】:
一旦我弄清楚如何在 TL 中实现这两个步骤;DR 我将发布代码。 我遇到了同样的问题,但在我的工作流程中我从不退出。通过 XHR 登录后,我根本无法完成 POST 操作。所以我的流程是通过 xhr 从新的小部件表单登录(控制台中没有 csrf 警告)-> 尝试提交新的小部件表单(控制台中的 csrf 警告)-> 注销并重定向到常规登录表单。根据控制台,在 xhr 请求登录和 html #create 请求期间,params[:authenticity_token]
是相同的。这里有什么帮助吗??
@sixty4bit 您是否扩展了Devise::SessionsController
以在:create
和:destroy
操作中返回新的当前csrf 令牌?然后,您的客户端代码需要从响应中读取和设置这些新值,并将它们与所有未来的请求一起发送。如果不清楚,请告诉我,稍后我会尝试发布我们当前工作的完整解决方案(可能需要一两天)。
抱歉,new_csrf_token
只是包含用户新 csrf 令牌的变量的占位符,所以是的,form_authenticity_token
是有道理的。这两条线看起来都不错。
@sixty4bit 关于您添加到 application.js 的 $(document).ajaxSend
行。您必须确保该行实际上在页面加载时运行。【参考方案3】:
Jimbo 出色地解释了您遇到的问题背后的“原因”。您可以采取两种方法来解决此问题:
(根据 Jimbo 的建议)重写 Devise::SessionsController 以返回新的 csrf-token:
class SessionsController < Devise::SessionsController
def destroy # Assumes only JSON requests
signed_out = (Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name))
render :json =>
'csrfParam' => request_forgery_protection_token,
'csrfToken' => form_authenticity_token
end
end
并在客户端为您的 sign_out 请求创建一个成功处理程序(可能需要根据您的设置进行一些调整,例如 GET 与 DELETE):
signOut: function()
var params =
dataType: "json",
type: "GET",
url: this.urlRoot + "/sign_out.json"
;
var self = this;
return $.ajax(params).done(function(data)
self.set("csrf-token", data.csrfToken);
self.unset("user");
);
这还假设您将 CSRF 令牌自动包含在所有 AJAX 请求中,如下所示:
$(document).ajaxSend(function (e, xhr, options)
xhr.setRequestHeader("X-CSRF-Token", MyApp.session.get("csrf-token"));
);
更简单地说,如果它适合您的应用程序,您可以简单地覆盖 Devise::SessionsController
并使用 skip_before_filter :verify_authenticity_token
覆盖令牌检查。
【讨论】:
有人知道这个问题是否已向 Devise 开发人员提出? #2 对我不起作用,因为我在通过 Devise/Ajax 登录并使用之后发出的任何 POST 请求后收到 CSRF 真实性错误。我也不确定如何渲染新的 csrf 令牌,因为我已经在:create
操作的最后一步渲染模板。我在这里提出了一个问题 (***.com/questions/26640326/…),如果您有时间查看一下,我将不胜感激
@sixty4bit 我似乎遇到了和你一样的问题,但是你的问题被删除了。你有想过吗?
请问您能否回答这个非常相似的问题? ***.com/questions/50159847/…【参考方案4】:
在挖掘 Warden 源之后,我注意到将 sign_out_all_scopes
设置为 false
会阻止 Warden 清除整个会话,因此 CSRF 令牌在退出之间保留。
Devise issue tacker 相关讨论:https://github.com/plataformatec/devise/issues/2200
【讨论】:
【参考方案5】:这是我的看法:
class SessionsController < Devise::SessionsController
after_filter :set_csrf_headers, only: [:create, :destroy]
respond_to :json
protected
def set_csrf_headers
if request.xhr?
response.headers['X-CSRF-Param'] = request_forgery_protection_token
response.headers['X-CSRF-Token'] = form_authenticity_token
end
end
end
在客户端:
$(document).ajaxComplete(function(event, xhr, settings)
var csrf_param = xhr.getResponseHeader('X-CSRF-Param');
var csrf_token = xhr.getResponseHeader('X-CSRF-Token');
if (csrf_param)
$('meta[name="csrf-param"]').attr('content', csrf_param);
if (csrf_token)
$('meta[name="csrf-token"]').attr('content', csrf_token);
);
每次您通过 ajax 请求返回 X-CSRF-Token
或 X-CSRF-Param
标头时,这将使您的 CSRF 元标记保持更新。
【讨论】:
谢谢,当我从我的控制器调用request_forgery_protection_token
和 form_authenticity_token
时,我不得不使用字符串插值,因为我收到一个错误,提示我无法在 :authenticity_token:String
上调用 split
...但是这种技术真的很好用。谢谢。
这是正确的答案,尽管我确实想知道重新生成新 crsf 令牌的安全隐患。例如,您应该添加 if self.request.format.symbol == :json 或您没有重定向的任何 mime
我根据您的建议更新了答案。很好的收获,谢谢!
@marflar 你能展示你在那里做了什么吗?我有个类似的问题。为 :authenticity_token:Symbol 获取 Unexpected error while processing request: undefined method
each'【参考方案6】:
我的回答大量借鉴了@Jimbo 和@Sija,但是我使用的是Rails CSRF Protection + Angular.js: protect_from_forgery makes me to log out on POST 建议的devise/angularjs 约定,并在我最初这样做时对我的blog 进行了一些详细说明。这在应用程序控制器上有一个方法来为 csrf 设置 cookie:
after_filter :set_csrf_cookie_for_ng
def set_csrf_cookie_for_ng
cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
end
所以我使用@Sija 的格式,但使用早期 SO 解决方案中的代码,给我:
class SessionsController < Devise::SessionsController
after_filter :set_csrf_headers, only: [:create, :destroy]
protected
def set_csrf_headers
cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
end
end
为了完整起见,由于我花了几分钟的时间来解决它,我还注意到需要修改您的 config/routes.rb 以声明您已覆盖会话控制器。比如:
devise_for :users, :controllers => sessions: 'sessions'
这也是我在我的应用程序上完成的大型 CSRF 清理的一部分,其他人可能会对此感兴趣。 blog post is here,其他变化包括:
从 ActionController::InvalidAuthenticityToken 中救援,这意味着如果事情不同步,应用程序将自行修复,而不是用户需要清除 cookie。在 Rails 中,我认为您的应用程序控制器将默认为:
protect_from_forgery with: :exception
在这种情况下,您需要:
rescue_from ActionController::InvalidAuthenticityToken do |exception|
cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
render :error => 'invalid token', :status => :unprocessable_entity
end
我也对竞态条件以及与 Devise 中的可超时模块的一些交互感到悲伤,我在博文中对此进行了进一步评论 - 简而言之,您应该考虑使用 active_record_store 而不是 cookie_store,并且要小心关于在 sign_in 和 sign_out 操作附近发出并行请求。
【讨论】:
【参考方案7】:我刚刚在我的布局文件中添加了它,它起作用了
<%= csrf_meta_tag %>
<%= javascript_tag do %>
jQuery(document).ajaxSend(function(e, xhr, options)
var token = jQuery("meta[name='csrf-token']").attr("content");
xhr.setRequestHeader("X-CSRF-Token", token);
);
<% end %>
【讨论】:
【参考方案8】:回复@sixty4bit 的评论;如果你遇到这个错误:
Unexpected error while processing request: undefined method each for :authenticity_token:Symbol`
替换
response.headers['X-CSRF-Param'] = request_forgery_protection_token
与
response.headers['X-CSRF-Param'] = request_forgery_protection_token.to_s
【讨论】:
这可能会回答评论中的问题,但因此不应该是对这个问题的回答。我建议你创建一个链接到这个问题的新问题,并用这个答案自我回答。然后,您还可以为此问题添加评论,链接到您新创建的问题【参考方案9】:就我而言,在用户登录后,我需要重新绘制用户的菜单。那行得通,但是我在同一部分对服务器的每个请求都收到了 CSRF 真实性错误(当然,没有刷新页面)。上面的解决方案不起作用,因为我需要渲染一个 js 视图。
我所做的是,使用设计:
app/controllers/sessions_controller.rb
class SessionsController < Devise::SessionsController
respond_to :json
# GET /resource/sign_in
def new
self.resource = resource_class.new(sign_in_params)
clean_up_passwords(resource)
yield resource if block_given?
if request.format.json?
markup = render_to_string :template => "devise/sessions/popup_login", :layout => false
render :json => :data => markup .to_json
else
respond_with(resource, serialize_options(resource))
end
end
# POST /resource/sign_in
def create
if request.format.json?
self.resource = warden.authenticate(auth_options)
if resource.nil?
return render json: status: 'error', message: 'invalid username or password'
end
sign_in(resource_name, resource)
render json: status: 'success', message: '¡User authenticated!'
else
self.resource = warden.authenticate!(auth_options)
set_flash_message(:notice, :signed_in)
sign_in(resource_name, resource)
yield resource if block_given?
respond_with resource, location: after_sign_in_path_for(resource)
end
end
end
之后,我向重绘菜单的控制器#action 发出请求。在 javascript 中,我修改了 X-CSRF-Param 和 X-CSRF-Token:
app/views/utilities/redraw_user_menu.js.erb
$('.js-user-menu').html('');
$('.js-user-menu').append('<%= escape_javascript(render partial: 'shared/user_name_and_icon') %>');
$('meta[name="csrf-param"]').attr('content', '<%= request_forgery_protection_token.to_s %>');
$('meta[name="csrf-token"]').attr('content', '<%= form_authenticity_token %>');
我希望它对处于相同 js 情况的人有用:)
【讨论】:
【参考方案10】:我的情况更简单。就我而言,我想做的只是:如果一个人坐在屏幕上,拿着一个表格,并且他们的会话超时(设计可超时会话超时),通常如果他们此时点击提交,设计会将他们弹回到登录屏幕。好吧,我不想这样,因为他们丢失了所有的表单数据。我使用 JavaScript 来捕获表单提交,Ajax 调用一个控制器来确定用户是否不再登录,如果是这种情况,我提出一个表单,让他们重新输入密码,然后重新验证它们(bypass_sign_in 在控制器中)使用 Ajax 调用。然后允许原始表单提交继续。
在我添加protect_from_forgery 之前一直运行良好。
所以,多亏了上面的答案,我真正需要的只是在我的控制器中让用户重新登录(bypass_sign_in),我只是将一个实例变量设置为新的 CSRF 令牌:
@new_csrf_token = form_authenticity_token
然后在渲染的 .js.erb 中(再一次,这是一个 XHR 调用):
$('meta[name="csrf-token"]').attr('content', '<%= @new_csrf_token %>');
$('input[type="hidden"][name="authenticity_token"]').val('<%= @new_csrf_token %>');
瞧。我的表单页面没有刷新,因此被旧令牌卡住了,现在我从我的用户登录获得的新会话中获得了新令牌。
【讨论】:
以上是关于Rails,设计认证,CSRF 问题的主要内容,如果未能解决你的问题,请参考以下文章
ember简单auth用rails设计用户名而不是电子邮件?
Django框架进阶7 django请求生命周期流程图, django中间件, csrf跨站请求伪造, auth认证模块