4

我的 Rails 4 + Devise 3.2 应用程序中的一项功能遇到了一个奇怪的问题,该功能允许用户通过 AJAX POST 将密码更改为以下操作,该操作源自 Devise wiki Allow users to edit their password似乎在用户更改密码并在以后的一个或多个请求之后,他们被强制注销,并且在重新登录后将继续被强制注销。

# 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 实例时,它在生产中失败。Unauthorized似乎发生的情况是,用户将保持登录状态,直到他们的请求之一到达不同的线程,然后他们以 401 响应状态注销。如果我使用单个线程和单个工作人员运行 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

2

由于您使用 Dalli 作为会话存储,因此您可能会遇到此问题。

多线程达利

从页面:

“如果您使用 Puma 或其他线程应用程序服务器,从 Dalli 2.7 开始,您可以使用带有 Rails 的 Dalli 客户端池,以确保 Rails.cache 单例不会成为线程争用的来源。”

于 2014-02-11T22:57:48.677 回答
1

我怀疑由于以下问题,您会看到这种行为:

  • devise 使用从warden 获取值的实例变量定义了 current_user 辅助方法。在lib/devise/controllers/helpers.rb#58。替换用户映射

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

我自己没有遇到这个,这是猜测,但希望它在某种程度上有所帮助。在多线程应用程序中,每个请求都被路由到一个线程,该线程可能由于缓存而保留 current_user 的先前值,或者在线程本地存储中,或者在可以跟踪每个线程数据的机架中。

一个线程更改底层数据(密码更改),使先前的数据无效。其他线程共享的缓存数据没有更新,导致后期使用陈旧数据访问导致强制注销。一种解决方案可能是标记密码已更改,允许其他线程检测到该更改并优雅地处理它,而无需强制注销。

于 2014-02-09T21:38:16.970 回答
0

这是一个粗略、粗略的解决方案,但似乎其他线程会对我的模型进行ActiveRecord 查询缓存User,并且返回的陈旧数据会触发身份验证失败。

通过采用绕过 ActiveRecord 缓存中描述的技术,我将以下内容添加到我的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)
}

我意识到这对我的应用程序具有普遍的性能影响,但这似乎是我可以解决这个问题的唯一方法。我尝试覆盖许多使用此查询的设计和看守方法,但没有运气。也许我会考虑尽快提交一个针对设计/监狱长的错误。

于 2014-02-27T20:48:26.913 回答
0

我建议在用户更改密码后,将其注销并清除其会话,如下所示:

  def update_password
    @user = User.find(current_user.id)
    if @user.update(user_params)
      sign_out @user # Let them sign-in again
      reset_session # This might not be needed?
      redirect_to root_path
    else
      render "edit"
    end
  end

我相信您的主要问题是sign_in更新会话的方式与您提到的多线程相结合。

于 2014-02-09T16:07:14.500 回答