2012-08-07 63 views
34

我正在做一个使用Rails的单页应用程序。登录和注销时使用ajax调用Devise控制器。我得到的问题是,当我1)登录2)注销然后再次登录不起作用。Rails,设计认证,CSRF问题

我认为它与CSRF令牌有关,当我注销时它会被重置(尽管它不应该失败),并且由于它是单页,旧的CSRF令牌正在xhr请求中发送,从而重置会话。

更具体的是这样的工作流程:

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

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

回答

35

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

  1. (所推荐的神保)重写设计:: SessionsController回报新CSRF令牌:

    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")); 
    }); 
    
  2. 更加简单,它是否适合你的应用程序,你可以简单地覆盖Devise::SessionsControllerskip_before_filter :verify_authenticity_token覆盖令牌检查。

+1

任何人都知道这个问题是否与Devise开发者一起提出? – IanWhalen 2012-12-29 04:10:33

+1

#2对我不起作用,因为我通过Devise/Ajax通过任何POST请求进行登录后收到CSRF真实性错误。我也不确定如何渲染新的csrf标记,因为我已经将模板渲染为我的':create'动作的最后一步。我在这里提出了一个问题(http://stackoverflow.com/questions/26640326/after-devise-sign-in-via-ajax-any-post-request-results-in-csrf-authenticity-err)并真的很感激,如果你有时刻检查出来 – sixty4bit 2014-10-29 21:36:19

+1

@ sixty4bit我似乎与你有同样的问题,但你的问题已被删除。你有没有发现它? – 2015-07-21 15:01:36

0

检查是否已包括这在你的application.js文件

//= require jquery

//= require jquery_ujs

之所以存在是jQuery的轨道宝石在默认情况下会自动将所有Ajax请求的CSRF令牌,需要这两个

+0

是的,我有这些。我使用开发工具检查了这些请求,它们都有CSRF。 – vrepsys 2012-08-07 12:17:14

+0

检查您是否获得相同的CSRF令牌或不同的令牌 – pdpMathi 2012-08-10 13:21:54

+0

此解决方案适用于我。 感谢和问候, – Icicle 2013-12-06 16:48:40

31

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

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

  • 您需要重写Devise SessionController :: destroy方法以返回您的新CSRF令牌。
  • 然后在客户端,您需要为注销XMLHttpRequest设置成功处理程序。在该处理程序,你需要从响应利用此新CSRF令牌,并设置它在你的DOM: $('meta[name="csrf-token"]').attr('content', <NEW_CSRF_TOKEN>)

更多详解你最有可能得到了protect_from_forgery集的ApplicationController.rb从该文件所有其他控制器都继承(我认为这很常见)。 protect_from_forgery对所有非GET HTML/Javascript请求执行CSRF检查。由于Devise Login是一个POST,它执行CSRF检查。如果CSRF检查失败,则用户的当前会话被清除,即将用户注销,因为服务器认为它是攻击(这是正确的/期望的行为)。

所以假设你在登出状态开始,你做一个新的页面加载,并且不会再重新加载页面:

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

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

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

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

  5. 然后你让一个需要登录的用户的另一个的XMLHttpRequest:当前会话没有登录的用户,以便制定回报401

更新:8/14设计注销不会给你一个新的CSRF令牌,通常在注销后发生的重定向会给你一个新的csrf令牌。

+0

一旦我找出如何实现TL中的两个步骤;我会发布代码。 – plainjimbo 2012-08-13 22:32:57

+0

来自DOM的CSRF值不是一个好的解决方案。 来自Angular Doc:[$ http](http://docs.angularjs.org/api/ng.$http) >由于只有在您的域上运行的JavaScript才能读取cookie,因此您的服务器可以得到保证XHR来自在您的域上运行的JavaScript。标题不会被设置为跨域请求。 – chakming 2014-04-10 06:27:20

+1

我有这个相同的问题,但在我的工作流程中,我从不退出。通过XHR登录后,我无法完成POST操作。因此,我的流程是通过xhr(在控制台中没有csrf警告)从新窗口小部件表单登录 - >尝试提交新窗口小部件窗体(控制台中的csrf警告) - >退出并重定向到常规登录窗体。根据控制台的说明,'params [:authenticity_token]'在xhr登录请求和html #create请求期间是相同的。任何帮助? – sixty4bit 2014-10-29 20:15:31

5

挖监狱长源后,我注意到,设置sign_out_all_scopesfalse从清除整个会话停止监狱长,所以CSRF令牌标志奏之间保留。

上设计问题装钉相关讨论:https://github.com/plataformatec/devise/issues/2200

+0

这个答案解决了我的许多问题。谢谢卢卡斯。 – Matt 2016-02-13 17:52:29

7

这是我的看法:

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); 
    } 
}); 

这将保证您的CSRF meta标签更新一次您通过ajax请求返回X-CSRF-TokenX-CSRF-Param标题。

+0

谢谢,我从我的控制器调用'request_forgery_protection_token'和'form_authenticity_token'时不得不使用字符串插值,因为我收到关于不能在':authenticity_token:String'上调用'split'的错误......但是这种技术可行真的很好。谢谢。 – stephenmurdoch 2014-02-06 12:45:00

+2

这是正确的答案,但我不知道重新生成一个新的crsf标记的安全含义。你应该添加如果self.request.format.symbol ==:例如json或任何你不重定向的mime – Gepsens 2014-04-22 11:27:28

+1

我根据你的建议更新了答案。好,赶快,谢谢! – Sija 2014-04-23 22:28:39

8

我的回答来自@Jimbo和@Sija大量借鉴,但是我使用的色器件/ angularjs会议建议在Rails CSRF Protection + Angular.js: protect_from_forgery makes me to log out on POST,并阐述了一点在我blog当我最初这样做。这样做的应用程序控制器上的方法来设置cookies的CSRF:

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的格式,但使用的代码从早期做方案,给我:

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操作附近的并行请求。

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

为@ sixty4bit的评论;如果你遇到这样的错误:

Unexpected error while processing request: undefined method each for :authenticity_token:Symbol` 

response.headers['X-CSRF-Param'] = request_forgery_protection_token.to_s 
+0

这可以回答评论中的问题,但不应该回答这个问题。我建议你创建一个新的问题链接到这个问题,并自我回答这个答案。然后,您也可以为链接到新创建的问题添加评论 – Bowdzone 2015-02-27 14:15:40

0

更换

response.headers['X-CSRF-Param'] = request_forgery_protection_token 

在我的情况后,在登录,我需要重新绘制用户的菜单中的用户。这很有效,但是在同一部分(当然,没有刷新页面),每次向服务器发送请求时都会收到CSRF真实性错误。以上解决方案无法正常工作,因为我需要呈现js视图。

我所做的这是什么,使用设计:

应用程序/控制器/ 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-帕拉姆和X-CSRF令牌:

应用程序/视图/公共事业/ 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情况:)