在我的CakePHP应用程序中,我有多租户,它通过独立的数据库提供(每个租户都有自己的,特定于租户的数据库)。
还有一个全球性的'包含用户和租赁信息的数据库。租户' table包含特定租户占用的数据库的名称。每个用户都包含一个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中,并且数据库参数被加载,因此他们自己的“隔离”了。模型可以使用这种动态连接。
但是,当用户尝试通过Basic Auth登录并直接请求他们想要访问的控制器功能时,会出现问题。例如/Orders/view/1.xml。 在这种情况下,CakePHP尝试构建“订单”。在用户登录之前的模型,因此在任何租赁信息可用之前 - 这意味着它不知道要连接到哪个数据库以访问订单。
从把debug()语句放到这个地方我可以看到构造/执行models / controllers / auth的顺序如下(执行/Orders/view/1.xml时):
我的问题是AuthComponent :: _ startup是在构建Order Model之后执行的。在此之前我需要尝试登录用户(并获取他们的数据库信息)' Order'模型构建。
问题:
==更新01/05/2014 == 插入代码示例。
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 Component不包含在AppController中的$ components中,但如果请求是api。*子域,则动态使用。但是,构造类的顺序不会影响BasicAlias AuthComponent是否包含在$ components中,或者如上所示动态使用。
AppModel:
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;
...
}
Tenant.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 auth凭证以及请求的情况下,会发生的是按以下顺序构造/执行类:
问题是:正在构建用户已登录的Order.php,这意味着当执行AppModel.php中的代码时:
$db_name = $Tenant->checkAndCreateTenantDatabaseConnectionForCurrentUser();
无法确定用户当前的租期。
我需要找到一个解决方法,要么通过以某种方式在构造Order.php之前执行登录,或者黑客攻击,以便在尝试构建具有$ use_tenant_database = true且用户不是的模型时登录后,此时执行BasicAuth尝试登录用户..但是这对我来说是错误的。
答案 0 :(得分:3)
您可能需要查看Cake的文档中的Authorization (who’s allowed to access what)部分。具体来看isAuthorized
函数及其工作原理。
您的订单控制器中可能需要这样的内容:
// app/Controller/OrdersController.php
public function isAuthorized($user) {
// All registered users can add posts
if ($this->action === 'add') {
return true;
}
// The owner of an order can edit and delete it
if (in_array($this->action, array('edit', 'delete'))) {
$orderId = (int) $this->request->params['pass'][0];
if ($this->Order->isOwnedBy($orderId, $user['id'])) {
return true;
}
}
return parent::isAuthorized($user);
}
答案 1 :(得分:0)
在app控制器中过滤请求生命周期回调之前实现逻辑。
Controller :: beforeFilter(): 此功能在控制器中的每个操作之前执行。这是检查活动会话或检查用户权限的便利位置。
答案 2 :(得分:0)
事实证明,这些模型是由“Search.Prg”构建的。插件,一个用于处理搜索/过滤结果的CakeDC插件。正在执行组件中的initialize()函数,并导致在用户登录之前构建模型。
解决这个问题的方法是将Basic Auth检查/登录过程从AppController beforeFilter移动到ExtendedAuthComponent(我自己的自定义authenciation组件)初始化函数。 结束代码是这样的:
ExtendedAuthComponent.php
public function initialize(Controller $controller)
{
parent::initialize($controller); // Call parent initialization first, this sets up request and response variables.
$this->controller = $controller;
// Reroute all requests to API subdomain (ie: api.rms.roving.net.au) 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)
{
$controller->params['action'] = 'api_'.$controller->params['action']; // prefix the actions with api_
if($this->loggedIn() == false) // Attempt Basic Auth Login
{ // Login failed
$this->authenticate = array('BasicAlias'); // Switch to using Basic Authentication
if($this->login() == false)
{
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!
}
}
}
}
这会导致用户在运行Search.Prg组件initialize()函数之前通过Basic Auth登录,这意味着在构建模型之前确定用户租期,从而解决问题。