43

Rails を使用して単一ページのアプリケーションを作成しています。サインインおよびサインアウトするとき、Devise コントローラーは ajax を使用して呼び出されます。私が得ている問題は、1) サインイン 2) サインアウトしてから再度サインインしても機能しないことです。

サインアウト時にリセットされる CSRF トークンに関連していると思いますが (それは間違いではありません)、単一ページであるため、古い CSRF トークンが xhr 要求で送信され、セッションがリセットされます。

より具体的には、これがワークフローです。

  1. ログイン
  2. サインアウト
  3. サインイン (成功した 201。ただしWARNING: Can't verify CSRF token authenticity、サーバー ログに出力されます)
  4. 後続の ajax リクエストが 401 未承認で失敗する
  5. Web サイトを更新します (この時点で、ページ ヘッダーの CSRF が別のものに変わります)。
  6. サインインできます。サインアウトして再度サインインするまでは機能します。

手がかりは大歓迎です!詳細を追加できるかどうか教えてください。

4

10 に答える 10

40

Jimbo は、あなたが直面している問題の背後にある「理由」を説明してくれました。この問題を解決するには、次の 2 つの方法があります。

  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

于 2012-09-27T14:45:37.783 に答える
34

私もこの問題に遭遇しました。ここでは多くのことが起こっています。

TL;DR - 失敗の理由は、CSRF トークンがサーバー セッションに関連付けられているためです (ログインしているかログアウトしているかに関係なく、サーバー セッションが存在します)。CSRF トークンは、ページが読み込まれるたびにページの DOM に含まれます。ログアウトすると、セッションがリセットされ、csrf トークンがなくなります。通常、ログアウトすると別のページ/アクションにリダイレクトされ、新しい CSRF トークンが提供されますが、ajax を使用しているため、これを手動で行う必要があります。

  • 新しい CSRF トークンを返すには、Devise SessionController::destroy メソッドをオーバーライドする必要があります。
  • 次に、クライアント側で、ログアウト XMLHttpRequest の成功ハンドラーを設定する必要があります。そのハンドラーでは、応答からこの新しい CSRF トークンを取得し、dom に設定する必要があります。 $('meta[name="csrf-token"]').attr('content', <NEW_CSRF_TOKEN>)

より詳細な説明ApplicationController.rb ファイルに、他のすべてのコントローラーが継承する設定がある可能性が最も高いprotect_from_forgeryです (これはかなり一般的だと思います)。protect_from_forgeryGET 以外のすべての HTML/Javascript リクエストに対して CSRF チェックを実行します。Devise Login は POST であるため、CSRF チェックを実行します。CSRF チェックが失敗した場合、ユーザーの現在のセッションはクリアされます。つまり、ユーザーはログアウトされます。これは、サーバーが攻撃であると見なすためです (これは正しい/望ましい動作です)。

したがって、ログアウトした状態で開始すると仮定すると、新しいページの読み込みを行い、ページを再度リロードすることはありません。

  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 によってヘッダーに設定されたトークンと、セッション情報に格納されているトークンを比較し、それらが一致するため、リクエストが成功します。

  3. その後、XMLHttpRequest 経由でログアウトします。これにより、セッションがリセットされ、CSRF トークンのない新しいセッションが提供されます。

  4. 次に、XMLHttpRequest を介して再度サインインします。jquery-ujs は、の値から CSRF トークンを取得します$('meta[name="csrf-token"]').attr('content')。この値は、古い CSRF トークンのままです。この古いトークンを取得し、それを使用して「X-CSRF-トークン」を設定します。サーバーは、このヘッダー値を、セッションに追加する新しい CSRF トークンと比較しますが、これは異なります。この違いにより、protect_form_forgeryが失敗し、 がスローWARNING: Can't verify CSRF token authenticityされてセッションがリセットされ、ユーザーがログアウトされます。

  5. 次に、ログイン ユーザーを必要とする別の XMLHttpRequest を作成します。現在のセッションにはログイン ユーザーがいないため、devise は 401 を返します。

更新: 8/14 Devise のログアウトでは、新しい CSRF トークンが提供されません。通常、ログアウト後に発生するリダイレクトによって、新しい csrf トークンが提供されます。

于 2012-08-13T22:32:21.363 に答える
8

これは私の見解です:

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またはヘッダーを返すたびに、CSRFメタタグが更新されます。X-CSRF-Param

于 2013-11-21T01:45:12.307 に答える
6

Warden ソースを掘り下げた後、Wardenがセッション全体をクリアするのを停止するsign_out_all_scopesように設定しているため、サインアウト間で CSRF トークンが保持されることに気付きました。false

Devise イシュー タッカーに関する関連ディスカッション: https://github.com/plataformatec/devise/issues/2200

于 2013-01-05T20:35:35.133 に答える
0

私の場合、ユーザーをログインした後、ユーザーのメニューを再描画する必要がありました。それはうまくいきましたが、サーバーへのすべてのリクエストで、同じセクションでCSRF認証エラーが発生しました(もちろん、ページを更新しませんでした)。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の状況にある誰かに役立つことを願っています:)

于 2016-07-15T05:22:05.703 に答える
0

これを application.js ファイルに含めたかどうかを確認してください

//= jquery が必要

//= jquery_ujs が必要

その理由は、デフォルトですべての Ajax リクエストに CSRF トークンを自動的に設定する jquery-rails gem であり、これら 2 つが必要です。

于 2012-08-07T12:13:55.827 に答える
0

私の状況はさらに単純でした。私の場合、私がやりたかったのはこれだけでした: 人がフォームで画面に座っていて、セッションがタイムアウトした場合 (Devise のタイムアウト可能なセッション タイムアウト)、通常、その時点で [送信] をクリックすると、Devise はそれらを跳ね返します。ログイン画面へ。フォーム データがすべて失われるためです。私は JavaScript を使用してフォームの送信をキャッチし、Ajax はユーザーがサインインしていないかどうかを判断するコントローラーを呼び出します。その場合は、パスワードを再入力するフォームを作成し、再認証します (コントローラーで bypass_sign_in)。 Ajax 呼び出しを使用します。その後、元のフォームの送信を続行できます。

protect_from_forgery を追加するまでは完全に機能していました。

したがって、上記の回答のおかげで、実際に必要だったのは、ユーザーを再度サインインするコントローラー (bypass_sign_in) だけでした。インスタンス変数を新しい CSRF トークンに設定しただけです。

@new_csrf_token = form_authenticity_token

次に、レンダリングされた .js.erb で (これも XHR 呼び出しであるため):

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

出来上がり。更新されなかったために古いトークンが残っていたフォーム ページに、ユーザーのサインインから取得した新しいセッションからの新しいトークンが含まれるようになりました。

于 2019-07-17T19:59:13.363 に答える
-1

@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
于 2015-02-27T13:54:51.963 に答える