设计和多租户范围

时间:2014-09-24 09:43:56

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

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

无论如何,我很想知道,为什么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

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

1 个答案:

答案 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
相关问题