私はRailsを使用して単一ページのアプリケーションを実行しています。サインインおよびサインアウトすると、Adevを使用してDeviseコントローラーが呼び出されます。私が得ている問題は、1)サインイン2)サインアウトしてから再度サインインしても機能しないことです。
私はサインアウトするとリセットされるCSRFトークンに関連していると思います(それは気づくべきではありませんが)、単一ページなので、古いCSRFトークンがxhrリクエストで送信され、セッションがリセットされます。
より具体的には、これがワークフローです。
WARNING: Can't verify CSRF token authenticity
サーバーログ内)どんな手がかりも大歓迎です!詳細を追加できるかどうかを教えてください。
神保は、あなたが直面している問題の背後にある「なぜ」を説明する素晴らしい仕事をしました。この問題を解決するには、次の2つの方法があります。
(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 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");
});
}
また、これは、すべてのAJAXリクエストに次のようなものが含まれるCSRFトークンを自動的に含めることを前提としています。
$(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トークンがサーバーセッションに関連付けられていることです(ログインしているかログアウトしているかにかかわらず、サーバーセッションがあります)。 CSRFトークンは、ページが読み込まれるたびにページのDOMに含まれます。ログアウトすると、セッションはリセットされ、csrfトークンはありません。通常、ログアウトは別のページ/アクションにリダイレクトされ、新しいCSRFトークンが提供されますが、ajaxを使用しているため、これを手動で行う必要があります。
$('meta[name="csrf-token"]').attr('content', <NEW_CSRF_TOKEN>)
詳細な説明 ApplicationController.rbファイルに_protect_from_forgery
_が設定されている可能性が高く、そこから他のすべてのコントローラーが継承されます(これはかなり一般的だと思います)。 _protect_from_forgery
_は、すべての非GET HTML/Javascript要求に対してCSRFチェックを実行します。 Devise LoginはPOSTであるため、CSRFチェックを実行します。 CSRFチェックが失敗した場合、サーバーは攻撃(正しい/望ましい動作)であると想定するため、ユーザーの現在のセッションはクリアされます。つまり、ユーザーがログアウトされます。
したがって、ログアウト状態で開始すると仮定すると、新しいページのロードを行い、ページを再度リロードすることはありません。
ページのレンダリング時:サーバーは、サーバーセッションに関連付けられたCSRFトークンをページに挿入します。このトークンを表示するには、browser$('meta[name="csrf-token"]').attr('content')
のJavaScriptコンソールから次を実行します。
その後、XMLHttpRequestを介してサインインしますこの時点でCSRFトークンは変更されないため、セッションのCSRFトークンはページに挿入されたものと一致します。舞台裏では、クライアント側で、jquery-ujsはxhrをリッスンし、自動的に$('meta[name="csrf-token"]').attr('content')
の値を持つ 'X-CSRF-Token'ヘッダーを設定します(これは、CSRFトークンがサーバーによるステップ1)。サーバーは、jquery-ujsによってヘッダーに設定されたトークンと、セッション情報に保存されているトークンを比較し、一致するように要求が成功します。
その後、XMLHttpRequestを介してログアウトします:これはセッションをリセットし、CSRFトークンのない新しいセッションを提供します。
その後、XMLHttpRequestを介して再度サインインします jquery-ujsは、$('meta[name="csrf-token"]').attr('content')
の値からCSRFトークンを取得します。この値はまだ[〜#〜] old [〜#〜] CSRFトークンです。この古いトークンを受け取り、それを使用して「X-CSRF-Token」を設定します。サーバーは、このヘッダー値を、セッションに追加する新しいCSRFトークンと比較しますが、これは異なります。この違いにより_protect_form_forgery
_が失敗し、_WARNING: Can't verify CSRF token authenticity
_がスローされ、セッションがリセットされてユーザーがログアウトされます。
ログインしたユーザーを必要とする別のXMLHttpRequestを作成します:現在のセッションにはログインしたユーザーがいないため、deviseは401を返します。
更新:8/14 Devise logoutは新しいCSRFトークンを提供しません。ログアウト後に通常発生するリダイレクトは、新しいcsrfトークンを提供します。
私の答えは@Jimboと@Sijaの両方から大きく借りていますが、 Rails CSRF Protection + Angular.js:protect_from_forgeryによりPOSTでログアウトする で提案されたdevise/angularjsの規則を使用しています blog で最初にこれをやったとき。これには、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ソリューションからのコードを使用して、私に与えます:
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をクリアする必要はなく、アプリケーション自体が修正されます。 Railsに立つと、アプリケーションコントローラーはデフォルトで次のようになります:
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のタイムアウト可能モジュールとのやり取りがありました。これについてはブログ投稿でさらにコメントしました。要するに、cookie_storeではなくactive_record_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);
}
});
CSRFメタタグは、X-CSRF-Token
またはX-CSRF-Param
ajaxリクエスト経由のヘッダー。
ワーデンのソースを掘り下げた後、sign_out_all_scopes
からfalse
は、Wardenがセッション全体をクリアするのを停止するため、サインアウトの間、CSRFトークンは保持されます。
Devise issue tackerの関連ディスカッション: https://github.com/plataformatec/devise/issues/22
これをレイアウトファイルに追加したところ、うまくいきました
<%= 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トークンを自動的に設定し、これら2つが必要です
私の場合、ユーザーをログインした後、ユーザーのメニューを再描画する必要がありました。それはうまくいきましたが、同じセクションでサーバーへのすべてのリクエストでCSRF認証エラーが発生しました(もちろんページを更新せずに)。上記のソリューションは、jsビューをレンダリングする必要があるため機能しませんでした。
Deviseを使用してこれを行いました:
app/controllers/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
その後、メニューを再描画するcontroller#actionにリクエストを行いました。そして、javascriptで、X-CSRF-ParamとX-CSRF-Tokenを変更しました:
app/views/utilities/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状況にいる人に役立つことを願っています:)
私の状況はさらに簡単でした。私の場合、私がしたかったのはこれだけです:ユーザーがフォームのある画面に座っていて、セッションがタイムアウトした場合(タイムアウト可能なセッションタイムアウトを回避する)、通常、その時点でSubmitを押すと、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 %>');
出来上がり。更新されなかったため、古いトークンで固定されていたフォームページには、ユーザーのサインインから取得した新しいセッションからの新しいトークンがあります。