【问题标题】:CakePHP perform Auth before Constructing Models?CakePHP 在构建模型之前执行 Auth?
【发布时间】: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 时):

  1. 模型 __construct:用户
  2. 控制器__construct:OrdersController
  3. 模型 __construct:权限
  4. 模型 __construct:订单
  5. 功能:OrdersController/beforeFilter
  6. AuthComponent __startup
  7. 模型 __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 身份验证凭据,那么会按以下顺序构造/执行类:

  1. 模型 __construct:用户
  2. 控制器__construct:OrdersController
  3. 模型 __construct:权限
  4. 模型 __construct:订单
  5. 模型 __construct:租户
  6. 模型 __construct:TenantDatabase
  7. 功能:OrdersController/beforeFilter
  8. AuthComponent __startup --> 然后执行登录。
  9. 模型 __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


【解决方案1】:

您可能想查看 Cake 文档中的 Authorization (who’s allowed to access what) 部分。具体看isAuthorized函数及其工作原理。

您的 Orders 控制器中可能需要这样的东西:

// 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);
}

【讨论】:

  • 您好。我不确定这会有什么帮助..您建议覆盖 isAuthorized() - 这样做的目的是什么?这个问题与模型/控制器的构建顺序和回调(即:beforeFilter)的执行顺序有关。我需要“选择”模型的数据库源,但是只有在触发登录后才能获取此信息。问题是模型是在登录执行之前构建的,所以我没有机会根据登录“选择”数据库源。
【解决方案2】:

在应用控制器中过滤请求生命周期回调之前实现您的逻辑。

Controller::beforeFilter() : 此函数在控制器中的每个操作之前执行。这是检查活动会话或检查用户权限的方便场所。

http://book.cakephp.org/2.0/en/controllers.html

【讨论】:

  • 您阅读了我的整个问题吗?我展示了每个控制器/模型的构建时间以及“beforeFilter”函数的执行时间。如果您阅读了这个问题,您会发现我的问题是因为 Orders 模型是在 OrdersController/beforeFilter 运行之前构建的 - 所以我无法通过使用 beforeFilter 来更改模型构建(即:使用哪个 db 源)。跨度>
【解决方案3】:

事实证明,这些模型是由“Search.Prg”插件构建的,这是一个用于处理搜索/过滤结果的 CakeDC 插件。组件中的 initialize() 函数正在执行,并导致在用户登录之前构建模型。

解决这个问题的方法是将基本身份验证检查/登录过程从 AppController beforeFilter 移动到 ExtendedAuthComponent(我自己的自定义身份验证组件)初始化函数。 结束代码是这样的:

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 登录,这意味着在构建模型之前确定用户的租户,从而解决问题。

【讨论】:

    猜你喜欢
    • 2013-09-03
    • 1970-01-01
    • 1970-01-01
    • 2017-06-11
    • 2012-05-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-08-04
    相关资源
    最近更新 更多