【发布时间】:2014-04-16 03:21:42
【问题描述】:
在我的 CakePHP 应用程序中,我有通过隔离数据库提供的多租户(每个租户都有自己的特定于租户的数据库)。
还有一个包含用户和租赁信息的“全球”数据库。 “租户”表包含特定租户占用的数据库的名称。每个用户都包含一个tenant_id。
结构:
global_db:
users (contains tenant_id foreign key)
tenants (contains tenant-specific database name, ie: 'isolated_tenant1_db')
isolated_tenant1_db:
orders
jobs
customers
isolated_tenant2_db:
orders
jobs
customers
当用户通过表单/会话登录时,此系统可以正常工作。当他们通过 /Users/login 登录时,他们的租约得到验证,存储在 Session 中,并加载数据库参数,因此他们自己的“隔离”模型可以使用此动态连接。
但是,当用户尝试通过基本身份验证登录并直接请求他们想要访问的控制器功能时,就会出现问题。例如 /Orders/view/1.xml。 在这种情况下,CakePHP 尝试在用户登录之前构建“订单”模型,因此在任何租赁信息可用之前 - 这意味着它不知道要连接到哪个数据库才能访问订单。
通过将 debug() 语句放在这个地方我可以看到构建/执行模型/控制器/身份验证的顺序如下(执行 /Orders/view/1.xml 时):
- 模型 __construct:用户
- 控制器__construct:OrdersController
- 模型 __construct:权限
- 模型 __construct:订单
- 功能:OrdersController/beforeFilter
- AuthComponent __startup
- 模型 __construct:与订单相关的模型
我的问题是 AuthComponent::_startup 在订单模型构建后执行。在构建此“订单”模型之前,我需要尝试登录用户(并获取他们的数据库信息)。
问题:
- 是什么导致 User 模型先于其他任何东西构建? (我也启用了默认的 CakePHP ACL)
- 如果请求包含 BasicAuth 标头,我可以在应用程序的哪个位置调用 Auth->login() 以尝试登录,这将在尝试加载特定于租户的模型之前执行?我认为将其放在 User __construct 中是一个非常糟糕的主意。
== 2014 年 1 月 5 日更新 == 插入代码示例。
bootstrap.php: 检查是否正在向 api 发出请求。子域:
// Determine whether the request is coming from the api.* subdomain, and if so set the API_REQUEST define to true.
if (preg_match('/^api\./i',$_SERVER['HTTP_HOST']))
{
define('API_REQUEST',true);
// Any links generated (in emails etc), will contain the full base url. If a cron job logged in via the API is generating
// those e-mails, then users will receive links to api.mydomain, instead of just mydomain.
$full_base_url = Router::fullBaseUrl();
$new_full_base_url = preg_replace('/\/\/api\./i', '//', $full_base_url);
Router::fullBaseUrl($new_full_base_url);
CakeLog::write('auth_base_url_debug', 'modified fullbaseurl from ' . $full_base_url . ' to ' . $new_full_base_url);
}
else
{
define('API_REQUEST',false);
}
AppController.php:
public $components = array(
'Security',
'Session',
'Acl',
'Auth' => array(
'className' => 'ExtendedAuth',
'authenticate' => array(
'FormAlias',
),
'authorize' => array(
'Actions' => array('actionPath' => 'controllers')
),
'loginRedirect' => array('controller' => 'Consignments', 'action' => 'index'),
'logoutRedirect' => array('controller' => 'Users', 'action' => 'login'),
),
//'Users.RememberMe',
);
function beforeFilter()
{
// Reroute all requests to API subdomain (ie: api.mydomain) to api_ prefixed actions.
// Also, enable Basic Authentication if the user is accessing via api.*
// If login fails, return 401 error instead of 302 redirect to login page.
if(API_REQUEST == true)
{
$this->params['action'] = 'api_'.$this->params['action']; // prefix the actions with api_
$this->Auth->authenticate = array('BasicAlias'); // Switch to using Basic Authentication
if($this->Auth->login() == false) // Attempt Basic Auth Login
{ // Login failed
CakeLog::write('auth_api', 'Unauthorized API request to: ' . $this->params['action']);
header("HTTP/1.0 401 Unauthorized"); // Force returning an Unauthorized header (401)
exit; // MUST BE CALLED TO PREVENT 302 BEING SENT!
}
}
}
需要注意的是,BasicAlias Auth 组件不包含在 AppController 中的 $components 中,但如果请求是 api.* 子域,则会动态使用。但是,无论 BasicAlias AuthComponent 是包含在 $components 中,还是如上所示动态使用,类的构造顺序都没有影响。
应用模型:
function __construct($id = false, $table = null, $ds = null)
{
if(($ds == null) && ($this->use_tenant_database == true))
{
// Create a connection to the tenants database and configure model to use this connection.
$Tenant = ClassRegistry::init('Tenant');
$db_name = $Tenant->checkAndCreateTenantDatabaseConnectionForCurrentUser();
if($db_name == false)
{
header("HTTP/1.0 500 Server Error"); // Force returning a Server Error Header (500)
debug('AppModel::$db_name = false, unable to proceed');
CakeLog::write('tenant_error', 'db_name = false, unable to connect.');
exit; // MUST BE CALLED TO PREVENT 302 BEING SENT!
}
// Point model to the tenant database connection:
$this->useDbConfig = $db_name;
}
parent::__construct($id, $table, $ds);
}
然后在使用特定租户数据库的任何模型中:
class Order extends AppModel
{
var $use_tenant_database = true;
...
}
租户.php:
/**
* Check whether a connection to the current users tenant database has already been created and if so, return its name.
* Otherwise, create the connection and return its name.
*
* @return boolean|Ambigous <mixed, multitype:, NULL, array, boolean>
*/
public function checkAndCreateTenantDatabaseConnectionForCurrentUser()
{
// Check whether we have the tenants database connection information available in the Configure variable:
if(Configure::check('Tenant.db_name') == true)
{ // the db_config is available in configure, use it!
$db_name = Configure::read('Tenant.db_name');
}
else
{ // The tenants db_name has not been set in the configure variable, we need to create a database connection and then
// set the configure variable.
$tenant_id = $this->getCurrentUserTenantId();
if($tenant_id == null)
{ // Unable to resolve the tenant_id, instead, connect to the default database.
debug('TRIED TO CONSTRUCT MODEL WITHOUT KNOWING TENANT DATABASE!!');
exit;
}
$db_name = $this->TenantDatabase->createConnection($tenant_id);
if($db_name == false)
{ // The database connection could not be created.
CakeLog::write('tenant_error', 'unable to find the database name for tenant_id: ' . $tenant_id);
return false;
}
Configure::write('Tenant.db_name', $db_name);
}
return $db_name;
}
因此,如果用户请求一个 URL,例如: http://api.mydomain.com/Orders/getAllPendingOrders 如果他们在请求中提供了 BASIC 身份验证凭据,那么会按以下顺序构造/执行类:
- 模型 __construct:用户
- 控制器__construct:OrdersController
- 模型 __construct:权限
- 模型 __construct:订单
- 模型 __construct:租户
- 模型 __construct:TenantDatabase
- 功能:OrdersController/beforeFilter
- AuthComponent __startup --> 然后执行登录。
- 模型 __construct:其他模型。
问题是:正在构建Order.php 用户已经登录,也就是说AppModel.php中的代码在执行时:
$db_name = $Tenant->checkAndCreateTenantDatabaseConnectionForCurrentUser();
无法确定用户当前的租期。
我需要找到一个解决方法,或者通过在构建 Order.php 之前以某种方式执行登录,或者破解它,以便如果您尝试构建一个具有 $use_tenant_database = true 的模型,而用户不是登录,然后此时执行 BasicAuth 以尝试登录用户.. 但是这对我来说感觉不对。
【问题讨论】:
-
模型是在首次访问时创建的。这意味着给定问题 tyst 中的信息,直接或间接引用 beforeFilter 中的订单模型。知道您可以随时重新定义数据源,这将从那时起生效。对于具体的答案,您需要显示您的代码。
-
@AD7six 感谢您的回复,我认为您可能是对的,但这似乎是一个非常奇怪的 hack,因为它需要您遍历 ClassRegistry 中的所有内容并调整所有构造的数据源楷模。理想情况下,AuthComponent 可以在构建 OrdersController 之前执行,但是我不确定这是否可以完成。
-
@AD7six 抱歉,我在没有 Eclipse 的情况下度过了一个复活节假期,所以直到今天我才能生成一大堆代码示例,因为我现在已经回来了。我已经更新了原始帖子,任何反馈/ cmets 将不胜感激。
-
这更有用。两件事:1)除非您在引导程序中创建它,否则用户模型不会在您的控制器之前被实例化 2)如果您将
debug(Debugger::trace())放入您的 Order 模型中 - 您会看到您是首先从 Acl 组件访问它已经。在详细说明如何更好地做你正在做的事情之前,确认这两点会很有用。 -
好的,按要求修改。
标签: cakephp cakephp-2.4