Rails,deviseauthentication,CSRF问题
我正在做一个使用Rails的单页应用程序。 login和注销Devise控制器使用ajax调用。 我得到的问题是,当我1)login2)注销然后再次login不起作用。
我认为这与CSRF令牌有关,当我退出时它会被重置(尽pipe它不应该是错误的),并且由于它是单页,因此旧的CSRF令牌正在xhr请求中发送,从而重置会话。
更具体的是这个工作stream程:
- 签到
- 登出
- login(成功201.但打印
WARNING: Can't verify CSRF token authenticity
服务器日志中的WARNING: Can't verify CSRF token authenticity
) - 随后的ajax请求失败401未经授权
- 刷新网站(此时,页眉中的CSRF更改为其他内容)
- 我可以login,它的工作,直到我试图退出,并在一次。
任何线索非常感谢! 让我知道如果我可以添加更多的细节。
Jimbo做了一个很棒的工作,解释了你遇到的问题背后的原因。 有两种方法可以解决这个问题:
-
(按照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")); });
-
更简单一些,如果它适合您的应用程序,您可以简单地重写
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检查失败,则清除用户的当前会话,即将用户注销,因为服务器认为这是一种攻击(这是正确的/期望的行为)。
所以假设你已经开始注销了,你会重新加载页面,并且不会再重新加载页面:
-
在呈现页面时:服务器将与服务器会话关联的CSRF令牌插入到页面中。 您可以通过在浏览器
$('meta[name="csrf-token"]').attr('content')
从javascript控制台运行以下代码来查看此令牌。 -
然后通过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头中设置的令牌和存储在会话信息中的令牌,它们匹配以便请求成功。 -
然后通过XMLHttpRequest注销:这将重置会话,为您提供没有CSRF令牌的新会话。
-
然后通过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
并重置会话,从而将用户注销。 -
然后,再创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-Token
或X-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