更改密码后用户会话无效,但只有多个线程

我的Rails 4 + Devise 3.2应用程序中的一个function遇到了一个奇怪的问题,它允许用户通过AJAX POST将其密码更改为以下操作,该操作源自Devise wiki 允许用户编辑其密码 。 似乎在用户更改密码之后以及之后的一个或多个请求之后,它们将被强制注销,并在重新登录后继续被强制注销。

# 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 

这个动作实际上在开发中工作正常,但是当我运行multithreading,多工作Puma实例时,它在生产中失败了。 出现的情况是用户将保持登录状态,直到其中一个请求命中不同的线程,然后他们以401响应状态注销为Unauthorized 。 如果我使用单个线程和单个工作程序运行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。 我的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 

那么……这里可能出现什么问题? 如何在密码更改后更新所有线程/工作人员的会话,而无需重新启动服务器?

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

multithreadingDalli

从页面:

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

我怀疑你是否因为以下问题而看到了这种行为:

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

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

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

一个线程更改基础数据(密码更改),使先前数据无效。 其他线程之间共享的缓存数据未更新,导致以后使用陈旧数据进行访问以导致强制注销。 一种解决方案可能是标记密码已更改,允许其他线程检测到该更改并正常处理,而不强制注销。

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

  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如你所提到的sign_in更新会话和multithreading的方式。

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

通过调整User.rb 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) } 

我意识到这会影响整个应用程序的性能,但它似乎是解决这个问题的唯一方法。 我尝试覆盖使用此查询的许多设计和监护方法,但没有运气。 也许我会尽快提出反对设计/监狱长的错误。