注意:原始问题有所改变。我找到了两个解决方案,可能正在改变设计的方式。
无论如何,我很想知道,为什么RequestStore不起作用(是因为Warden在中间件堆栈中拦截了前面的消息?),Thread.current是如何工作的,以及为什么实例变量是一个不稳定的解决方案
我在我的应用程序中使用default_scope启用了多租户,包括Devise用户模型。
在application_controller.rb中,我有
around_filter :set_request_store
def set_request_store
Tenant.current = current_tenant.id
yield
ensure
Tenant.current = nil
end
Tenant.current又设置了一个RequestStore哈希键。
在tenant.rb
def self.current
RequestStore.store[:current_tenant_id]
end
def self.current=(tenant_id)
RequestStore.store[:current_tenant_id] = tenant_id
end
在我的routes.rb文件中,我有以下
unauthenticated do
root to: 'home#index', as: :public_root
end
authenticated :user do
root to: 'dashboard#index', as: :application_root
end
通过日志可以更好地说明我遇到的问题。
成功登录后。
Started POST "/users/sign_in" for 127.0.0.1 at 2014-09-24 14:57:13 +0530
Processing by Devise::SessionsController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"[FILTERED]", "user"=>{"tenant_id"=>"1", "email"=>"user@example.com", "password"=>"[FILTERED]"}}
Tenant Load (0.9ms) SELECT "tenants".* FROM "tenants" WHERE "tenants"."subdomain" = 'test' ORDER BY "tenants"."id" ASC LIMIT 1
User Load (0.8ms) SELECT "users".* FROM "users" WHERE "users"."tenant_id" = 1 AND "users"."email" = 'user@example.com' ORDER BY "users"."id" ASC LIMIT 1
(0.2ms) BEGIN
SQL (0.5ms) UPDATE "users" SET "current_sign_in_at" = $1, "last_sign_in_at" = $2, "sign_in_count" = $3, "updated_at" = $4 WHERE "users"."id" = 1 [["current_sign_in_at", "2014-09-24 09:27:13.553818"], ["last_sign_in_at", "2014-09-24 09:26:31.548568"], ["sign_in_count", 44], ["updated_at", "2014-09-24 09:27:13.556155"]]
(1.1ms) COMMIT
Devise将应用程序重定向到(应用程序)根路径。实际上,公共和应用程序根源的路径是相同的。
Redirected to http://test.com.dev/
Completed 302 Found in 90ms (ActiveRecord: 3.4ms)
路由中未经身份验证的方法调用(可能)尝试对用户进行身份验证(在中间件的某处使用Warden ???),并且此时未设置tenant_id。请参阅tenant_id的WHERE子句。
Started GET "/" for 127.0.0.1 at 2014-09-24 14:57:13 +0530
User Load (0.8ms) SELECT "users".* FROM "users" WHERE "users"."tenant_id" IS NULL AND "users"."id" = 1 ORDER BY "users"."id" ASC LIMIT 1
Processing by HomeController#index as HTML
有人遇到过这样的问题并解决了吗?
解决方案1:
首先,我使用Thread.current解决了它。由于某种原因,RequestStore.store没有在Devise方法中设置。
以下代码解决了登录问题。但是,我找不到安全取消Thread.current值的地方。
在user.rb
中devise ...,
request_keys: [:subdomain]
default_scope { where(tenant_id: (Tenant.current || Thread.current[:current_tenant_id])) }
protected
def self.find_for_authentication(warden_conditions)
subdomain = warden_conditions.delete(:subdomain)
Thread.current[:current_tenant_id] = Tenant.where(subdomain: subdomain).first.id
super
end
解决方案2:
更新:这也有问题。这不会一直有效。
改为使用实例变量。
在user.rb
中devise ...,
request_keys: [:subdomain]
default_scope { where(tenant_id: (Tenant.current || @tenant_id)) }
protected
def self.find_for_authentication(warden_conditions)
subdomain = warden_conditions.delete(:subdomain)
@tenant_id = Tenant.where(subdomain: subdomain).first.id
super
end
我想知道哪种方法更安全,或者是否有更好的解决方法。
答案 0 :(得分:5)
我更喜欢依赖专用模块:
module TenantScope
extend self
class Error < StandardError
end
def current
threadsafe_storage[:current]
end
def current=(tenant)
threadsafe_storage[:current] = tenant
end
def with(tenant)
previous_scope = current
raise Error.new("Tenant can't be nil in #{self.name}.with") if tenant.nil?
self.current = tenant
yield(current) if block_given?
ensure
self.current = previous_scope
nil
end
private
def threadsafe_storage
Thread.current[:tenant_scope] ||= {}
end
end
然后,我将它用于对象的default_scope。只需include TenantScope::ModelMixin
在您的模型中(但不在租户中):
module TenantScope
module ModelMixin
def self.included(base)
base.belongs_to :tenant
base.validates_presence_of :tenant_id
base.send(:default_scope, lambda {
if TenantScope.current
return base.where("#{base.table_name}.tenant_id" => TenantScope.current.id)
end
raise Error.new('Scoped class method called without a tenant being set')
})
end
end
end
我使用中间件来设置范围。
module TenantScope
class Rack
attr_reader :request
def initialize(app)
@app = app
end
def call(env)
@request = ::Rack::Request.new(env)
unless tenant = Tenant.find_from_host(@request.host)
logger.error "[TenantScope] tenant not found: #{request.host}"
return [404, { 'Content-Type' => 'text/plain', 'Content-Length' => '29' }, ["This tenant does not exist"]]
end
logger.debug "[TenantScope] tenant found: #{tenant.name}"
TenantScope.with(tenant) do
@app.call(env)
end
end
def logger
Rails.logger
end
end
end
使用中间件,并确保每次访问模型都发生在此中间件的保护伞下。对于控制器中发生的所有事情都是如此。
我给了你一些线索。您看到我非常严格,并且必须始终设置租户,即使在迁移期间或在控制台中也是如此。
请注意,现在,要浏览所有用户,例如在迁移中,您必须执行以下操作:
Tenant.each do |tenant|
TenantScope.with(tenant) do
User.all.each do |user|
# do your stuff here.
end
end
end