Ruby-on-rails Rails,设计身份验证,CSRF 问题

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/11845500/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-03 03:49:12  来源:igfitidea点击:

Rails, Devise authentication, CSRF issue

ruby-on-railsajaxauthenticationdevisecsrf

提问by vrepsys

I'm doing a singe-page application using Rails. When signing in and out Devise controllers are invoked using ajax. The problem I'm getting is that when I 1) sign in 2) sign out then signing in again doesn't work.

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

I think it's related to CSRF token which gets reset when I sign out (though it shouldn't afaik) and since it's single page, the old CSRF token is being sent in xhr request thus resetting the session.

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

To be more concrete this is the workflow:

更具体地说,这是工作流程:

  1. Sign in
  2. Sign out
  3. Sign in (successful 201. However prints WARNING: Can't verify CSRF token authenticityin server logs)
  4. Subsequent ajax request fails 401 unauthorised
  5. Refresh the website (at this point, CSRF in the page header changes to something else)
  6. I can sign in, it works, until I try to sign out and in again.
  1. 登入
  2. 登出
  3. 登录(成功 201。但是WARNING: Can't verify CSRF token authenticity在服务器日志中打印)
  4. 后续ajax请求失败401未授权
  5. 刷新网站(此时页眉中的CSRF更改为其他内容)
  6. 我可以登录,它可以工作,直到我尝试退出并再次登录。

Any clues very much appreciated! Let me know if I can add any more details.

非常感谢任何线索!如果我可以添加更多详细信息,请告诉我。

采纳答案by jredburn

Jimbo did an awesome job explaining the "why" behind the issue you're running into. There are two approaches you can take to resolve the issue:

Jimbo 出色地解释了您遇到的问题背后的“原因”。您可以采用两种方法来解决此问题:

  1. (As recommended by Jimbo) Override Devise::SessionsController to return the new 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
    

    And create a success handler for your sign_out request on the client side (likely needs some tweaks based on your setup, e.g. 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");
      });
    }
    

    This also assumes you're including the CSRF token automatically with all AJAX requests with something like this:

    $(document).ajaxSend(function (e, xhr, options) {
      xhr.setRequestHeader("X-CSRF-Token", MyApp.session.get("csrf-token"));
    });
    
  2. Much more simply, if it is appropriate for your application, you can simply override the Devise::SessionsControllerand override the token check with skip_before_filter :verify_authenticity_token.

  1. (由 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");
      });
    }
    

    这还假设您在所有 AJAX 请求中自动包含 CSRF 令牌,如下所示:

    $(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

回答by plainjimbo

I've just run into this problem as well. There's a lot going on here.

我也刚遇到这个问题。这里发生了很多事情。

TL;DR- The reason for the failure is that the CSRF token is associated with your server session (you've got a server session whether you're logged in or logged out). The CSRF token is included in the DOM your page on every page load. On logout, your session is reset and has no csrf token. Normally, a logout redirects to a different page/action, which gives you a new CSRF token, but since you're using ajax, you need to do this manually.

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

  • You need to override the Devise SessionController::destroy method to return your new CSRF token.
  • Then on the client side you need to set a success handler for your logout XMLHttpRequest. In that handler you need to take this new CSRF token from the response and set it in your dom: $('meta[name="csrf-token"]').attr('content', <NEW_CSRF_TOKEN>)
  • 您需要覆盖 Devise SessionController::destroy 方法以返回您的新 CSRF 令牌。
  • 然后在客户端,您需要为注销 XMLHttpRequest 设置成功处理程序。在该处理程序中,您需要从响应中获取这个新的 CSRF 令牌并将其设置在您的 dom 中: $('meta[name="csrf-token"]').attr('content', <NEW_CSRF_TOKEN>)

More Detailed ExplanationYou've most likely got protect_from_forgeryset in your ApplicationController.rb file from which all of your other controllers inherit (this is pretty common I think). protect_from_forgeryperforms CSRF checks on all non-GET HTML/Javascript requests. Since Devise Login is a POST, it performs a CSRF Check. If a CSRF Check fails then the user's current session is cleared, i.e., logs the user out, because the server assumes it's an attack (which is the correct/desired behavior).

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

So assuming you're starting in a logged out state, you do a fresh page load, and never reload the page again:

因此,假设您以注销状态开始,您会重新加载页面,并且不再重新加载页面:

  1. On rendering the page:the server inserts the CSRF Token associated with your server session into the page. You can view this token by running the following from a javascript console in your browser$('meta[name="csrf-token"]').attr('content').

  2. You then Sign In via an XMLHttpRequest:Your CSRF Token remains unchanged at this point so the CSRF Token in your Session still matches the one that was inserted into the page. Behind the scenes, on the client side, jquery-ujs is listening for xhr's and setting a 'X-CSRF-Token' header with the value of $('meta[name="csrf-token"]').attr('content')for you automatically (remember this was the CSRF Token set in step 1 by the sever). The server compares the Token set in the header by jquery-ujs and the one that is stored in your session information and they match so the request succeeds.

  3. You then Log Out via an XMLHttpRequest:This resets session, gives you a new session without a CSRF Token.

  4. You then Sign In again via an XMLHttpRequest:jquery-ujs pulls the CSRF token from the value of $('meta[name="csrf-token"]').attr('content'). This value is still your OLDCSRF token. It takes this old token and uses it to set the 'X-CSRF-Token'. The server compares this header value with a new CSRF token that it adds to your session, which is different. This difference causes the protect_form_forgeryto fail, which throws the WARNING: Can't verify CSRF token authenticityand resets your session, which logs the user out.

  5. You then make another XMLHttpRequest that requires a logged in user:The current session doesn't have a logged in user so devise returns a 401.

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

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

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

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

  5. 然后创建另一个需要登录用户的 XMLHttpRequest:当前会话没有登录用户,因此设计返回 401。

Update: 8/14Devise logout does not give you a new CSRF token, the redirect that normally happens after a logout gives you a new csrf token.

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

回答by PaulL

My answer borrows heavily from both @Jimbo and @Sija, however I'm using the devise/angularjs convention suggested at Rails CSRF Protection + Angular.js: protect_from_forgery makes me to log out on POST, and elaborated a little on my blogwhen I originally did this. This has a method on the application controller to set cookies for csrf:

我的回答来自@Jimbo和@Sija大量借鉴,但是我使用的色器件/ angularjs会议建议在Rails的CSRF保护+ Angular.js: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

So I'm using @Sija's format, but using the code from that earlier SO solution, giving me:

所以我使用@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

For completeness, since it took me a couple of minutes to work it out, I also note the need to modify your config/routes.rb to declare that you've overridden the sessions controller. Something like:

为了完整起见,因为我花了几分钟来解决这个问题,我还注意到需要修改您的 config/routes.rb 以声明您已经覆盖了会话控制器。就像是:

devise_for :users, :controllers => {sessions: 'sessions'}

This was also part of a large CSRF cleanup that I've done on my application, which might be interesting to others. The blog post is here, the other changes include:

这也是我在我的应用程序上完成的大型 CSRF 清理的一部分,其他人可能会对此感兴趣。该博客文章在这里,其他的变化包括:

Rescuing from ActionController::InvalidAuthenticityToken, which means that if things get out of synch the application will fix itself, rather than the user needing to clear cookies. As things stand in rails I think your application controller will be defaulted with:

从 ActionController::InvalidAuthenticityToken 中拯救,这意味着如果事情不同步,应用程序将自行修复,而不是用户需要清除 cookie。就 Rails 而言,我认为您的应用程序控制器将默认为:

protect_from_forgery with: :exception

In that situation, you then need:

在这种情况下,您需要:

rescue_from ActionController::InvalidAuthenticityToken do |exception|
  cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
  render :error => 'invalid token', {:status => :unprocessable_entity}
end

I've also had some grief with race conditions and some interactions with the timeoutable module in Devise, which I've commented on further in the blog post - in short you should consider using the active_record_store rather than cookie_store, and be careful about issuing parallel requests near to sign_in and sign_out actions.

我也对竞争条件和与 Devise 中的 timeoutable 模块的一些交互感到有些悲痛,我在博客文章中对此进行了进一步评论 - 简而言之,您应该考虑使用 active_record_store 而不是 cookie_store,并注意发出并行请求靠近 sign_in 和 sign_out 操作。

回答by Sija

This is my take:

这是我的看法:

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

And on the client side:

而在客户端:

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

Which will keep your CSRF meta tags updated every time you return X-CSRF-Tokenor X-CSRF-Paramheader via ajax request.

每次通过ajax请求返回X-CSRF-TokenX-CSRF-Param标头时,这将使您的CSRF元标记保持更新。

回答by Lucas

After digging on the Warden source, I noticed that setting sign_out_all_scopesto falsestops Warden from clearing the entire session, so the CSRF token is preserved between sign outs.

在挖掘 Warden 源代码后,我注意到设置sign_out_all_scopesfalse阻止 Warden 清除整个会话,因此 CSRF 令牌在注销之间保留。

Related discussion on Devise issue tacker: https://github.com/plataformatec/devise/issues/2200

设计问题追踪器相关讨论:https: //github.com/plataformatec/devise/issues/2200

回答by r15

I just added this in my layout fileand it worked

我刚刚在我的布局文件中添加了它并且它起作用了

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

回答by pdpMathi

Check whether you have included this in your application.js file

检查您是否已将其包含在 application.js 文件中

//= require jquery

//= require jquery_ujs

//= 需要 jquery

//= 需要 jquery_ujs

The reason being is jquery-rails gem which automatically sets the CSRF token on all Ajax requests by default, needs those two

原因是 jquery-rails gem 默认情况下会在所有 Ajax 请求上自动设置 CSRF 令牌,需要这两个

回答by JGutierrezC

In my case, after login the user in, i needed to redraw the user's menu. That worked, but i got CSRF authenticity errors on every request to the server, in that same section (without refreshing the page, of course). Above solutions wasn't working since i needed to render a js view.

就我而言,在用户登录后,我需要重绘用户的菜单。那行得通,但是在同一部分(当然,没有刷新页面),我对服务器的每个请求都遇到了 CSRF 真实性错误。上述解决方案不起作用,因为我需要呈现 js 视图。

What i did is this, using Devise:

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

app/controllers/sessions_controller.rb

应用程序/控制器/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

After that i made a request to the controller#action that redraw the menu. And in the javascript, i modified the X-CSRF-Param and X-CSRF-Token:

之后,我向重绘菜单的控制器#action 发出请求。在 javascript 中,我修改了 X-CSRF-Param 和 X-CSRF-Token:

app/views/utilities/redraw_user_menu.js.erb

应用程序/视图/实用程序/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 %>');

I hope it's useful for someone on the same js situation :)

我希望它对处于相同 js 情况的人有用:)

回答by Jason Perrone

My situation was even simpler. In my case, all I wanted to do was this: if a person is sitting on a screen with a form, and their session times out (Devise timeoutable session timeout), normally if they hit Submit at that point, Devise would bounce them back to the login screen. Well, I didn't want that, because they lose all their form data. I use JavaScript to catch the form submit, Ajax call a controller which determines if the user is no longer signed in, and if that's the case I put up a form where they retype their password, and I reauthenticate them (bypass_sign_in in a controller) using an Ajax call. Then the original form submit is allowed to continue.

我的情况更简单。就我而言,我想要做的就是:如果一个人坐在带有表单的屏幕上,并且他们的会话超时(设计超时会话超时),通常如果他们当时点击提交,设计会将他们退回到登录屏幕。好吧,我不想那样,因为他们丢失了所有的表单数据。我使用 JavaScript 来捕获表单提交,Ajax 调用一个控制器来确定用户是否不再登录,如果是这种情况,我会在表单中重新输入密码,然后重新验证它们(控制器中的bypass_sign_in)使用 Ajax 调用。然后原始表单提交被允许继续。

Was working perfectly until I added protect_from_forgery.

在我添加protect_from_forgery 之前一直运行良好。

So, thanks to the above answers all I needed really was in my controller where I sign the user back in (the bypass_sign_in) I just set an instance variable to the new CSRF token:

因此,由于上述答案,我真正需要的只是在我的控制器中让用户重新登录(bypass_sign_in),我只是将一个实例变量设置为新的 CSRF 令牌:

@new_csrf_token = form_authenticity_token

and then in the .js.erb that was rendered (since again, this was an XHR call):

然后在渲染的 .js.erb 中(再次,这是一个 XHR 调用):

$('meta[name="csrf-token"]').attr('content', '<%= @new_csrf_token %>');
$('input[type="hidden"][name="authenticity_token"]').val('<%= @new_csrf_token %>');

Voila. My form page, which was not refreshed and therefore was stuck with the old token, now has the new token from the new session I got from signing in my user.

瞧。我的表单页面没有刷新,因此被旧令牌卡住了,现在有了我从用户登录时获得的新会话中的新令牌。

回答by Egbert Veenstra

in reply to a comment of @sixty4bit; if you run into this error:

回复@sixty4bit 的评论;如果您遇到此错误:

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

replace

代替

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

with

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