Rails,deviseauthentication,CSRF问题

我正在做一个使用Rails的单页应用程序。 login和注销Devise控制器使用ajax调用。 我得到的问题是,当我1)login2)注销然后再次login不起作用。

我认为这与CSRF令牌有关,当我退出时它会被重置(尽pipe它不应该是错误的),并且由于它是单页,因此旧的CSRF令牌正在xhr请求中发送,从而重置会话。

更具体的是这个工作stream程:

  1. 签到
  2. 登出
  3. login(成功201.但打印WARNING: Can't verify CSRF token authenticity服务器日志中的WARNING: Can't verify CSRF token authenticity
  4. 随后的ajax请求失败401未经授权
  5. 刷新网站(此时,页眉中的CSRF更改为其他内容)
  6. 我可以login,它的工作,直到我试图退出,并在一次。

任何线索非常感谢! 让我知道如果我可以添加更多的细节。

Jimbo做了一个很棒的工作,解释了你遇到的问题背后的原因。 有两种方法可以解决这个问题:

  1. (按照Jimbo的build议)重写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请求创build一个成功处理程序(可能需要根据您的设置进行一些调整,例如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")); }); 
  2. 更简单一些,如果它适合您的应用程序,您可以简单地重写Devise::SessionsController并使用skip_before_filter :verify_authenticity_token覆盖标记检查。

我也遇到了这个问题。 这里有很多事情要做

TL; DR – 失败的原因是CSRF令牌与服务器会话相关联(无论您是login还是注销,您都有一个服务器会话)。 CSRF令牌包含在每页加载的DOM页面中。 在注销时,您的会话被重置并且没有csrf标记。 通常情况下,注销redirect到一个不同的页面/动作,这给你一个新的CSRF令牌,但由于你使用的是Ajax,你需要手动执行此操作。

  • 你需要重写Devise SessionController :: destroy方法来返回你的新的CSRF令牌。
  • 然后在客户端,您需要为注销XMLHttpRequest设置成功处理程序。 在该处理程序中,您需要从响应中获取这个新的CSRF标记,并将其设置在您的dom中: $('meta[name="csrf-token"]').attr('content', <NEW_CSRF_TOKEN>)

更详细的解释你最有可能从您的ApplicationController.rb文件中设置protect_from_forgery ,从其中所有的其他控制器inheritance(这是很常见,我认为)。 protect_from_forgery对所有非GET HTML / Javascript请求执行CSRF检查。 由于deviselogin是一个POST,它执行一个CSRF检查。 如果CSRF检查失败,则清除用户的当前会话,即将用户注销,因为服务器认为这是一种攻击(这是正确的/期望的行为)。

所以假设你已经开始注销了,你会重新加载页面,并且不会再重新加载页面:

  1. 在呈现页面时:服务器将与服务器会话关联的CSRF令牌插入到页面中。 您可以通过在浏览器$('meta[name="csrf-token"]').attr('content')从javascript控制台运行以下代码来查看此令牌。

  2. 然后通过XMLHttpRequestlogin:您的CSRF令牌在此处保持不变,因此会话中的CSRF令牌仍然与插入到页面中的CSRF令牌相匹配。 在后台,在客户端,jquery-ujs正在侦听xhr,并设置$('meta[name="csrf-token"]').attr('content')的值为'X-CSRF-Token $('meta[name="csrf-token"]').attr('content')自动(请记住这是由服务器在步骤1中设置的CSRF令牌)。 服务器比较jquery-ujs头中设置的令牌和存储在会话信息中的令牌,它们匹配以便请求成功。

  3. 然后通过XMLHttpRequest注销:这将重置会话,为您提供没有CSRF令牌的新会话。

  4. 然后通过XMLHttpRequest再次login: jquery-ujs从$('meta[name="csrf-token"]').attr('content')的值拉取CSRF标记。 这个值仍然是您的 CSRF令牌。 它需要这个旧的令牌,并使用它来设置“X-CSRF-令牌”。 服务器比较这个头值和它添加到你的会话中的新的CSRF令牌,这是不同的。 这种差异会导致protect_form_forgery失败,从而引发WARNING: Can't verify CSRF token authenticity并重置会话,从而将用户注销。

  5. 然后,再创build一个需要login用户的XMLHttpRequest:当前会话没有login用户,所以devise返回一个401。

更新:8/14devise注销不会给你一个新的CSRF令牌,注销后通常发生的redirect给你一个新的csrf令牌。

我的答案很大程度上从@Jimbo和@Sija中借用,但是我使用的是Rails CSRF Protection + Angular.js中提出的devise / angularjs约定:protect_from_forgery使我能够注销POST ,并且在我的博客上详细阐述原本是这样做的。 这在应用程序控制器上为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解决scheme的代码,给我:

 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清理的一部分,这对其他人可能是有趣的。 博客文章在这里 ,其他的变化包括:

从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操作。

这是我的承担:

 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-TokenX-CSRF-Param标头时,这将保持您的CSRF元标记更新。

挖掘守望者源代码后,我注意到将sign_out_all_scopes设置为false阻止Warden清除整个会话,所以CSRF令牌在签名之间保留。

关于devise问题的讨论的相关讨论: https : //github.com/plataformatec/devise/issues/2200

我刚刚在我的布局文件中添加了这个工作

  <%= 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 %> 

检查你是否在你的application.js文件中包含了这个

// =需要jquery

// =需要jquery_ujs

原因是jquery-rails gem在默认情况下自动设置所有Ajax请求上的CSRF标记,需要这两者

在我的情况下,login用户后,我需要重新绘制用户的菜单。 这工作,但我得到了CSRF的真实性错误的每个请求到服务器,在同一节(当然没有刷新页面)。 上面的解决scheme是不工作,因为我需要呈现一个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 %>'); 

我希望这是有用的人在同一个JS的情况:)

在回复@ 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