我正在使用Rails进行单页应用程序。登录和注销时,使用ajax调用Devise控制器。我得到的问题是,当我1)登录2)退出然后再次登录不起作用。
我认为它与CSRF令牌有关,当我退出时它会被重置(虽然它不应该是afaik),因为它是单页,旧的CSRF令牌正在xhr请求中发送,从而重置会话。
更具体地说,这是工作流程:
WARNING: Can't verify CSRF token authenticity
)非常感谢任何线索!如果我可以添加更多细节,请告诉我。
答案 0 :(得分:36)
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 vs 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
覆盖令牌检查。
答案 1 :(得分:31)
我也遇到了这个问题。这里有很多事情要做。
TL; DR - 失败的原因是CSRF令牌与您的服务器会话相关联(无论您是登录还是退出,都有服务器会话)。 CSRF令牌包含在每个页面加载的DOM页面中。注销时,会话将重置,并且没有csrf令牌。通常,注销会重定向到不同的页面/操作,从而为您提供新的CSRF令牌,但由于您使用的是ajax,因此需要手动执行此操作。
$('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令牌保持不变,因此会话中的CSRF令牌仍然与插入页面的CSRF令牌相匹配。在幕后,在客户端,jquery-ujs正在监听xhr并自动为您设置值为$('meta[name="csrf-token"]').attr('content')
的“X-CSRF-Token”标头(请记住这是第1步中设置的CSRF令牌)由服务员)。服务器通过jquery-ujs比较标头中设置的令牌和会话信息中存储的令牌,它们匹配,因此请求成功。
然后通过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令牌。
答案 2 :(得分:9)
我的回答大量借用了@Jimbo和@Sija,但是我使用了Rails CSRF Protection + Angular.js: protect_from_forgery makes me to log out on POST中建议的设计/ 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。我认为您的应用程序控制器将默认为:
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操作附近发出并行请求。
答案 3 :(得分:8)
这是我的看法:
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元标记。
答案 4 :(得分:6)
在深入研究Warden源代码后,我注意到将sign_out_all_scopes
设置为false
会阻止Warden清除整个会话,因此在签出之间会保留CSRF令牌。
关于设计问题攻击者的相关讨论:https://github.com/plataformatec/devise/issues/2200
答案 5 :(得分:1)
我刚刚在布局文件中添加了它,并且它已经工作了
<%= 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 %>
答案 6 :(得分:0)
检查您是否已将其包含在application.js文件中
// = require jquery
// = require jquery_ujs
原因是jquery-rails gem默认在所有Ajax请求上自动设置CSRF令牌,需要那两个
答案 7 :(得分:0)
就我而言,在登录用户后,我需要重新绘制用户的菜单。这有效,但我在服务器的每个请求上都得到了CSRF真实性错误,在同一部分(当然没有刷新页面)。由于我需要渲染js视图,因此上述解决方案无法正常工作。
我用的是Devise:
应用程序/控制器/ 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
之后我向控制器#动作发出了重绘菜单的请求。在javascript中,我修改了X-CSRF-Param和X-CSRF-Token:
应用程序/视图/公用事业/ 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 %>');
我希望它对同样情况下的某些人有用:)
答案 8 :(得分:0)
我的情况更加简单。就我而言,我要做的就是:如果某人正坐在带有表单的屏幕上,并且他们的会话超时(Devise超时会话超时),通常如果他们在那一刻点击Submit,Devise会将他们反弹到登录屏幕。好吧,我不想这么做,因为他们会丢失所有表格数据。我使用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 %>');
Voila。我的表单页面没有刷新,因此被旧令牌卡住了,现在从我登录用户获得的新会话中获得了新令牌。
答案 9 :(得分:-1)
回复@ 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