2014-09-24 85 views
1

注:原来的问题已经改变了一下。我找到了两种解决方案,并可能在完全改变设计的方式上。在任何情况下,我都会想知道,为什么RequestStore不工作(是因为Warden在中间件堆栈中拦截消息?),Thread.current如何工作,以及为什么实例变量是不稳定的解。设计和多租户范围


我已经在我的应用程序中使用default_scope启用了多租户,包括Devise User模型。

在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 "https://stackoverflow.com/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"=>"[email protected]", "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" = '[email protected]' 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 

在一个成功的标志。设计重定向应用(应用程序)的根路径。实际上,公共和应用程序根目录的路径是相同的。

Redirected to http://test.com.dev/ 
Completed 302 Found in 90ms (ActiveRecord: 3.4ms) 

在路由未经身份验证的方法调用(可能)试图给用户(在中间件???使用看守的地方)认证和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:

UPDATE:这也有问题。这不总是工作。

改为使用实例变量。

在用户。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 

我想知道这将是一个更安全的方法,或是否有解决的更好的方法。

回答

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 
+0

我原来的问题解决更优雅的使用这种方法。谢谢! – 2014-10-02 10:27:09

+0

这个发现是一个绝对的创业板。有没有这个测试套件或宝石? – 2014-10-12 22:38:38