【问题标题】:Devise and Multi-tenant Scoping设计和多租户范围
【发布时间】:2014-09-30 04:11:00
【问题描述】:

注意:原来的问题有所改变。我找到了两个解决方案,并且可能正在完全改变设计。

无论如何,我很想知道,为什么 RequestStore 不起作用(是因为 Warden 在中间件堆栈中截获了前面的消息吗?),Thread.current 是如何工作的,以及为什么实例变量是一个不稳定的解决方案.


我已在我的应用程序中使用 default_scope 启用多租户,包括设计用户模型。

在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

我想知道哪种方法更安全,或者是否有更好的方法来解决这个问题。

【问题讨论】:

    标签: ruby-on-rails devise multi-tenant


    【解决方案1】:

    我更喜欢依赖专用模块:

    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
    

    【讨论】:

    • 我使用这种方法更优雅地解决了原始问题。谢谢!
    • 这个发现是绝对的 GEM。有测试套件或 gem 吗?
    • 这是天才兄弟。天才。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-03-21
    • 2011-10-25
    • 1970-01-01
    • 2023-01-03
    • 2023-03-29
    • 2012-09-24
    相关资源
    最近更新 更多