Ruby on rails Rails、设计认证、CSRF问题

Ruby on rails Rails、设计认证、CSRF问题,ruby-on-rails,ajax,authentication,devise,csrf,Ruby On Rails,Ajax,Authentication,Devise,Csrf,我正在使用Rails做一个单页应用程序。在登录和注销时,使用ajax调用Device控制器。我遇到的问题是,当我1)登录2)注销时,再次登录不起作用 我认为这与CSRF令牌有关,当我注销时,CSRF令牌会被重置(尽管它不应该是afaik),因为它是单页的,所以旧的CSRF令牌会在xhr请求中发送,从而重置会话 更具体地说,这是工作流程: 登录 注销 登录(成功201。但是打印警告:无法在服务器日志中验证CSRF令牌的真实性) 后续ajax请求未经授权而失败 刷新网站(此时,页面标题中的CSRF将

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

我认为这与CSRF令牌有关,当我注销时,CSRF令牌会被重置(尽管它不应该是afaik),因为它是单页的,所以旧的CSRF令牌会在xhr请求中发送,从而重置会话

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

  • 登录
  • 注销
  • 登录(成功201。但是打印
    警告:无法在服务器日志中验证CSRF令牌的真实性
  • 后续ajax请求未经授权而失败
  • 刷新网站(此时,页面标题中的CSRF将更改为其他内容)
  • 我可以登录,它工作,直到我尝试注销并再次登录

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

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

    //=需要jquery

    //=需要jquery\u ujs


    原因是jqueryrailsgem默认情况下会自动在所有Ajax请求上设置CSRF令牌,需要这两个标记,我刚才也遇到了这个问题。这里发生了很多事情

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

    • 您需要重写designe SessionController::destroy方法以返回新的CSRF令牌
    • 然后在客户端,您需要为注销XMLHttpRequest设置一个成功处理程序。在该处理程序中,您需要从响应中获取这个新的CSRF令牌,并将其设置在dom中:
      $('meta[name=“csrf token”]').attr('content',)
    更详细的解释您最有可能在ApplicationController.rb文件中设置了
    protect\u from\u forgery
    ,您的所有其他控制器都从该文件继承(我认为这很常见)<代码>防止伪造对所有非GET HTML/Javascript请求执行CSRF检查。由于Desive登录是一个POST,因此它执行CSRF检查。如果CSRF检查失败,则会清除用户的当前会话,即注销用户,因为服务器认为这是攻击(这是正确/期望的行为)

    因此,假设您在注销状态下启动,您将重新加载页面,并且不再重新加载页面:

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

  • 然后通过XMLHttpRequest登录:此时您的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令牌。此值仍然是您的CSRF令牌。它接受这个旧令牌并使用它设置“X-CSRF-token”。服务器将此头值与添加到会话中的新CSRF令牌进行比较,这是不同的。此差异导致
    保护表单伪造失败,引发
    警告:无法验证CSRF令牌的真实性
    并重置会话,从而注销用户

  • 然后进行另一个需要登录用户的XMLHttpRequest:当前会话没有登录用户,因此designe返回401


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

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

  • (按照Jimbo的建议)覆盖Desive::SessionController以返回新的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
    
    这还假设您在所有AJAX请求中自动包含CSRF令牌,如下所示:

    $(document).ajaxSend(function (e, xhr, options) {
      xhr.setRequestHeader("X-CSRF-Token", MyApp.session.get("csrf-token"));
    });
    
  • 更简单地说,如果它适合您的应用程序,您可以简单地覆盖
    designe::sessioncontroller
    ,并使用
    skip\u before\u filter:verify\u authenticity\u token
    覆盖令牌检查


  • 在挖掘了Warden源代码后,我注意到将
    sign_out\u all_scopes
    设置为
    false
    会阻止Warden清除整个会话,因此在注销之间会保留CSRF令牌

    有关设计问题解决者的相关讨论:

    这是我的观点:

    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
    

    每次您通过ajax请求返回
    X-CSRF-Token
    X-CSRF-Param
    头时,都会更新您的CSRF元标记。

    我的答案大量借用了@Jimbo和@Sija,但我没有
    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);
      }
    });
    
    after_filter  :set_csrf_cookie_for_ng
    
    def set_csrf_cookie_for_ng
      cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
    end
    
    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
    
    devise_for :users, :controllers => {sessions: 'sessions'}
    
    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
    
        <%= 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 %>
    
    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
    
       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
    
      $('.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 %>');
    
    @new_csrf_token = form_authenticity_token
    
    $('meta[name="csrf-token"]').attr('content', '<%= @new_csrf_token %>');
    $('input[type="hidden"][name="authenticity_token"]').val('<%= @new_csrf_token %>');