我遇到了一个奇怪的问题,我的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。我的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
那么......这里可能出现什么问题?如何在密码更改后更新所有线程/工作人员的会话,而无需重新启动服务器?
答案 0 :(得分:2)
由于您使用Dalli作为会话存储,因此您可能遇到此问题。
从页面:
“如果您使用Puma或其他线程应用服务器,从Dalli 2.7开始,您可以使用Rails的Dalli客户端池来确保Rails.cache单例不会成为线程争用的来源。”
答案 1 :(得分:1)
我怀疑你因为以下问题而看到了这种行为:
devise使用获取warden值的实例变量定义current_user帮助器方法。
在lib/devise/controllers/helpers.rb
#58。替换用户进行映射
def current_#{mapping}
@current_#{mapping} ||= warden.authenticate(:scope => :#{mapping})
end
我自己没有碰到这个,这是猜测,但希望它在某种程度上有所帮助。在多线程应用程序中,每个请求都被路由到一个线程,该线程可能会因为缓存而保留current_user的先前值,无论是在线程本地存储还是可能跟踪每个线程数据的机架中。
一个线程更改基础数据(密码更改),使先前数据无效。其他线程之间共享的缓存数据未更新,导致以后使用陈旧数据进行访问以导致强制注销。一种解决方案可能是标记密码已更改,允许其他线程检测到该更改并正常处理,而不强制注销。
答案 2 :(得分: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
如你所提到的那样更新会话和多线程的方式。
答案 3 :(得分:0)
这是一个粗略的粗略解决方案,但似乎其他线程会对User
模型进行ActiveRecord query caching,并且返回的陈旧数据会触发身份验证失败。
通过调整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)
}
我意识到这会影响整个应用程序的性能,但它似乎是解决问题的唯一方法。我尝试覆盖使用此查询的许多设计和监护方法,但没有运气。也许我会尽快提出反对设计/监狱长的错误。