4

Rails 4 + Devise 3.2 アプリケーションの機能で、ユーザーが AJAX POST を介して次のアクションにパスワードを変更できるようにする機能で奇妙な問題が発生していますユーザーがパスワードを変更した後、後で 1 つ以上の要求を行った後、強制的にログアウトされ、再度サインインした後も強制的にログアウトされ続けるようです。

# POST /update_my_password
def update_my_password
  @user = User.find(current_user.id)
  authorize! :update, @user ## CanCan check here as well

  if @user.valid_password?(params[:old_password])
    @user.password = params[:new_password]
    @user.password_confirmation = params[:new_password_conf]
    if @user.save
      sign_in @user, :bypass => true
      head :no_content
      return
    end
  else
    render :json => { "error_code" => "Incorrect password" }, :status => 401     
    return
  end

  render :json => { :errors => @user.errors }, :status => 422
end

このアクションは実際には開発では問題なく機能しますが、マルチスレッド、マルチワーカーの Puma インスタンスを実行している場合、本番環境では失敗します。発生しているように見えるのは、リクエストの 1 つが別のスレッドに到達するまでユーザーがログインしたままになり、その後Unauthorized401 応答ステータスでログアウトされることです。単一のスレッドと単一のワーカーで Puma を実行すると、問題は発生しません。ユーザーが複数のスレッドで再度ログインしたままにできるようにする唯一の方法は、サーバーを再起動することです (これは解決策ではありません)。私が持っているセッションストレージ構成がそれを正しく処理すると思っていたので、これはかなり奇妙です。私のconfig/initializers/session_store.rbファイルには以下が含まれています:

MyApp::Application.config.session_store(ActionDispatch::Session::CacheStore, :expire_after => 3.days)

私のproduction.rb設定には以下が含まれます:

config.cache_store = :dalli_store, ENV["MEMCACHE_SERVERS"],
{ 
  :pool_size => (ENV['MEMCACHE_POOL_SIZE'] || 1),
  :compress => true,
  :socket_timeout => 0.75, 
  :socket_max_failures => 3, 
  :socket_failure_delay => 0.1,
  :down_retry_delay => 2.seconds,
  :keepalive => true,
  :failover => true
}

経由でプーマを起動していbundle exec puma -p $PORT -C ./config/puma.rbます。私のpuma.rb内容:

threads ENV['PUMA_MIN_THREADS'] || 8, ENV['PUMA_MAX_THREADS'] || 16
workers ENV['PUMA_WORKERS'] || 2
preload_app!

on_worker_boot do
  ActiveSupport.on_load(:active_record) do
    config = Rails.application.config.database_configuration[Rails.env]
    config['reaping_frequency'] = ENV['DB_REAP_FREQ'] || 10 # seconds
    config['pool']              = ENV['DB_POOL'] || 16
    ActiveRecord::Base.establish_connection(config)
  end
end

それで...ここで何がうまくいかないのでしょうか?パスワードが変更されたときに、サーバーを再起動せずにすべてのスレッド/ワーカーでセッションを更新するにはどうすればよいですか?

4

4 に答える 4

1

次の問題が原因で、この動作が発生していると思われます。

  • devise は、warden から値を取得するインスタンス変数を使用して current_user ヘルパー メソッドを定義します。lib/devise/controllers/helpers.rb#58で。マッピングの代替ユーザー

    def current_#{mapping}
      @current_#{mapping} ||= warden.authenticate(:scope => :#{mapping})
    end
    

これは憶測ですが、何らかの形で役立つことを願っています。マルチスレッド アプリでは、スレッド ローカル ストレージまたはスレッドごとにデータを追跡する可能性があるラックのいずれかで、キャッシングのために current_user の以前の値を保持している可能性があるスレッドに、各要求がルーティングされます。

1 つのスレッドが基になるデータを変更し (パスワードの変更)、以前のデータを無効にします。他のスレッド間で共有されるキャッシュされたデータは更新されないため、古いデータを使用して後でアクセスすると、強制ログアウトが発生します。解決策の 1 つは、パスワードが変更されたことを示すフラグを立てて、他のスレッドがその変更を検出し、強制ログアウトせずに正常に処理できるようにすることです。

于 2014-02-09T21:38:16.970 に答える
0

これは大雑把な解決策ですが、他のスレッドが私のモデルのActiveRecord クエリ キャッシュUserを実行し、返された古いデータが認証の失敗を引き起こすように見えました。

Bypassing ActiveRecord cacheで説明されている手法を採用して、User.rbファイルに次の内容を追加しました。

# this default scope avoids query caching of the user,
# which can be a big problem when multithreaded user password changing
# happens. 
FIXNUM_MAX = (2**(0.size * 8 -2) -1)
default_scope { 
  r = Random.new.rand(FIXNUM_MAX)
  where("? = ?", r,r)
}

これには、アプリケーション全体に広がるパフォーマンスへの影響があることは認識していますが、この問題を回避できる唯一の方法のようです。このクエリを使用する多くのデバイスとウォーデン メソッドをオーバーライドしようとしましたが、うまくいきませんでした。おそらく、devise/warden に対するバグの報告をすぐに検討する予定です。

于 2014-02-27T20:48:26.913 に答える