卓越飞翔博客卓越飞翔博客

卓越飞翔 - 您值得收藏的技术分享站
技术文章16333本站已运行3317

CakePHP 访问控制列表:使用指南

CakePHP 访问控制列表:使用指南

如果您正在构建 CMS,您可能需要具有不同权限级别的不同用户角色(超级用户、管理员、用户)。代码太复杂?输入 CakePHP 的 ACL(访问控制列表)。通过正确的设置,您只需一行即可检查用户权限。

简介:什么是访问控制列表?

ACL 允许您创建具有各自角色的用户层次结构。这是一个简单的示例。

  • 超级用户
    • 用户 #1
  • 管理员
    • 用户#2
    • 用户#3
  • 用户
    • 用户#4
    • 用户#5
    • 用户#6
    • ...

在本教程中,我们将为一个简单的博客设置 ACL。如果您尚未在 Nettuts+ 上查看 CakePHP 入门(和第 2 部分),请查看后返回,因为我们将理所当然地认为框架基础知识。

通过这个层次结构,我们可以为每个角色分配多个权限:

  • 超级用户可以创建、阅读、更新和删除帖子和用户。
  • 管理员可以创建、阅读、更新和删除帖子。
  • 用户可以创建和阅读帖子。
  • 其他人都可以阅读帖子。

每个权限都会授予组,而不是用户;因此,如果用户 #6 晋升为管理员,系统将检查他的组权限——而不是他的权限。这些角色和子节点(用户)称为访问请求对象,或 ARO。

现在,在另一边,我们有了访问控制对象(ACO)。这些都是要控制的对象。上面我提到了帖子和用户。通常,这些对象与模型直接链接,因此如果我们有一个 Post 模型,我们将需要该模型的 ACO。

每个ACO都有四种基本权限:创建、读取、更新和删除。您可以使用关键字 CRUD 来记住它们。还有第五个权限,即星号,它是完全访问权限的快捷方式。

在本教程中,我们将仅使用两个 ACO:Post 和 User,但您可以根据需要创建任意多个 ACO。

ACL 表

让我们继续创建数据库表。您可以在应用程序的 config/sql 目录中的 db_acl.sql 中找到此代码。

CREATE TABLE acos ( 
  id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, 
  parent_id INTEGER(10) DEFAULT NULL, 
  model VARCHAR(255) DEFAULT '', 
  foreign_key INTEGER(10) UNSIGNED DEFAULT NULL, 
  alias VARCHAR(255) DEFAULT '', 
  lft INTEGER(10) DEFAULT NULL, 
  rght INTEGER(10) DEFAULT NULL, 
  PRIMARY KEY  (id) 
); 
 
CREATE TABLE aros_acos ( 
  id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, 
  aro_id INTEGER(10) UNSIGNED NOT NULL, 
  aco_id INTEGER(10) UNSIGNED NOT NULL, 
  _create CHAR(2) NOT NULL DEFAULT 0, 
  _read CHAR(2) NOT NULL DEFAULT 0, 
  _update CHAR(2) NOT NULL DEFAULT 0, 
  _delete CHAR(2) NOT NULL DEFAULT 0, 
  PRIMARY KEY(id) 
); 
 
CREATE TABLE aros ( 
  id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, 
  parent_id INTEGER(10) DEFAULT NULL, 
  model VARCHAR(255) DEFAULT '', 
  foreign_key INTEGER(10) UNSIGNED DEFAULT NULL, 
  alias VARCHAR(255) DEFAULT '', 
  lft INTEGER(10) DEFAULT NULL, 
  rght INTEGER(10) DEFAULT NULL, 
  PRIMARY KEY  (id) 
);

我们现在可以开始创建 ARO 和 ACO 节点,但是嘿,我们没有用户!我们必须创建一个基本的身份验证系统。


第 1 步:基本身份验证系统

由于本教程面向具有基本到中等框架知识的 CakePHP 开发人员,因此我将提供代码和简要说明。但是,身份验证系统不是本教程的目标。

MySQL 表:

CREATE TABLE users ( 
  id INTEGER(10) UNSIGNED AUTO_INCREMENT KEY, 
  username TEXT, 
  password TEXT 
);

用户模型 (models/user.php)

<?php 
class User extends AppModel { 
    var $name = 'User'; 
} 
?>

用户控制器 (controllers/users_controller.php)

<?php 
class UsersController extends AppController { 
    var $name = 'Users'; 
    var $components = array('Auth'); 
 
    function beforeFilter(){ 
        $this->Auth->userModel = 'User'; 
        $this->Auth->allow('*'); 
    } 
 
    function register(){ 
        if(!empty($this->data)){ 
            // Here you should validate the username (min length, max length, to not include special chars, not existing already, etc) 
            // As well as the password 
            if($this->User->validates()){ 
                $this->User->save($this->data); 
                // Let's read the data we just inserted 
                $data = $this->User->read(); 
                // Use it to authenticate the user 
                $this->Auth->login($data); 
                // Then redirect 
                $this->redirect('/'); 
            } 
        } 
    } 
 
    function login(){ 
        if(!empty($this->data)){ 
            // If the username/password match 
            if($this->Auth->login($this->data)){ 
                $this->redirect('/'); 
            } else { 
                $this->User->invalidate('username', 'Username and password combination is incorrect!'); 
            } 
        } 
    } 
 
    function logout(){ 
        $this->Auth->logout(); 
        $this->redirect('/'); 
    } 
} 
?>

既然我们有了新元素,让我们回顾一下它们。首先,我们设置一个 $components 变量。该变量包括数组中的所有组件。我们将需要 Auth 组件,它是一个核心组件,就像 HTML 和表单助手一样,但由于 Cake 默认情况下不包含它,所以我们必须手动包含它。

Auth 组件处理一些基本的身份验证机制:它帮助我们登录用户并为我们处理经过身份验证的用户会话,以及处理访客的注销和基本授权。此外,它还会自动对密码进行哈希处理。我将在下面的段落中解释如何调用每个函数。

接下来,我们创建一个名为 beforeFilter 的函数。这是一个回调函数,它允许我们在处理所有控制器逻辑之前设置一些操作。 Auth 组件要求我们指定要使用的模型,在本例中为 User。然后,默认情况下,它将拒绝所有未登录用户的访问。我们必须使用 allow() 覆盖此行为,该行为需要一个参数。该参数可以是星号,指定未经身份验证的用户可以访问所述控制器内的所有方法。或者,可以向它传递一个数组,其中包含未经身份验证的用户可以访问的函数。在本例中,由于我们只有三个函数,因此以下几行是相同的内容。

$this->Auth->allow('*'); 
$this->Auth->allow(array('register', 'login', 'logout'));

对于 login() 函数,Auth 组件将为我们处理所有登录机制。我们只需为该函数提供一个包含两个键的数组:用户名和密码。这些键可以更改,但默认情况下, usernamepassword 字段都将与数据库进行匹配,如果用户已通过身份验证,则返回 true。

最后,控制器的登录功能将尝试将用户名/密码组合与数据库进行匹配。

请注意,此代码非常基础。您需要验证用户名字符、用户名是否存在、密码的最小长度等等。

注册视图 (views/users/register.ctp)

<h2>Register your account</h2> 
 
<form method="POST" action="<?=$this->here; ?>"> 
 
<p> 
    Username 
    <?=$form->text('User.username'); ?> 
</p> 
<p> 
    Password 
    <?=$form->password('User.password'); ?> 
</p> 
 
<?=$form->submit('Register'); ?> 
</form>

登录视图 (views/users/login.ctp)

<h2>Log in to your account</h2> 
 
<form method="POST" action="<?=$this->here; ?>"> 
 
<?=$form->error('User.username'); ?> 
<p> 
    Username 
    <?=$form->text('User.username'); ?> 
</p> 
<p> 
    Password 
    <?=$form->password('User.password'); ?> 
 
</p> 
<?=$form->submit('Log in'); ?> 
</form>

在网络浏览器中打开 /users/register 并注册一个新帐户。我建议 admin 作为用户名,123 作为密码,如果您的会话过期,只需转到 /users/login 并输入您刚刚创建的正确用户名/密码组合。


第 2 步:拒绝未经身份验证的用户访问

我们甚至没有使用 ACL,但我们已经可以拒绝发布、编辑和删除帖子。打开您的 Posts 控制器并添加 Auth 组件。

var $components = array('Auth');

现在在您的网络浏览器中访问 /posts。如果您已登录,您应该会看到这些帖子,但如果您没有登录,您将被重定向到 /users/login。通过简单地包含身份验证组件,默认情况下,所有操作都会拒绝来宾。我们需要拒绝未经授权的用户的三种操作:创建、编辑和删除。换句话说,我们必须允许索引和视图。

function beforeFilter(){ 
    $this->Auth->userModel = 'User'; 
    $this->Auth->allow(array('index', 'view')); 
}

去编辑或创建帖子;如果您尚未登录,您应该被重定向到 /users/login。一切似乎都进展顺利,但是视图呢?编辑和删除链接正在向所有人显示。我们应该提出一个条件。

但在讨论之前,让我们看看 Auth 的 user() 函数是如何工作的。将这些行复制并粘贴到索引函数中。

$user = $this->Auth->user(); 
pr($user);

在浏览器中打开 /posts,如果登录,则 pr() 将抛出类似这样的内容。

Array 
( 
    [User] => Array 
        ( 
            [id] => 1 
            [username] => admin 
        ) 
)

user() 函数返回一个数组,就像模型一样。如果我们有超过三个字段(不包括密码),它们将显示在数组中。如果您没有登录,该数组将为空,因此如果 Auth 的 user() 数组不为空,您就可以知道用户已登录。

现在,Auth 是一个组件,旨在在控制器中使用。我们需要从视图中了解用户是否已登录,最好是通过帮助程序。我们如何在助手中使用组件? CakePHP 是如此出色和灵活,以至于这是可能的。

<? 
class AccessHelper extends Helper{ 
    var $helpers = array("Session"); 
 
    function isLoggedin(){ 
        App::import('Component', 'Auth'); 
        $auth = new AuthComponent(); 
        $auth->Session = $this->Session; 
        $user = $auth->user(); 
        return !empty($user); 
    } 
?>

将此代码段保存在 views/helpers 中,命名为 access.php。现在让我们逐行查看代码。首先,我们设置一个 $helpers 变量。帮助程序可以包含其他帮助程序,就像 $components 一样。 Auth 组件需要 Session 组件,但我们无法在助手中访问该组件。幸运的是,我们有一个会话助手,它将帮助我们。

接下来,我们创建一个函数并使用 App::import ,这将让我们导入一个通常我们无法访问的元素。下一行在 $auth 变量中创建 Auth 组件,现在是一个有点肮脏的黑客;由于 Auth 组件读取会话来了解我们是否已登录,因此它需要 Session 组件,但当我们从不应该属于它的位置导入它时,我们必须给它一个新的 Session 对象。最后,我们使用 user() 并将其设置为 $user ,如果变量不为空则返回 true,否则返回 false。

让我们回到帖子控制器并继续添加助手。

var $helpers = array('Access');

现在可以从视图访问访问助手。在 views/posts 中打开 index.ctp 并替换此行。

<small><a href="/posts/edit/<? echo $post['Post']['id'] ?>">edit</a> | <? echo $html->link('delete', '/posts/delete/'.$post['Post']['id'], NULL, 'Are you sure?'); ?></small>

有了这个。

<? if($access->isLoggedin()): ?><small><a href="/posts/edit/<? echo $post['Post']['id'] ?>">edit</a> | <? echo $html->link('delete', '/posts/delete/'.$post['Post']['id'], NULL, 'Are you sure?'); ?></small><? endif; ?>

返回网络浏览器,重新加载索引页,如果您已登录,您将看到每个帖子的编辑和删除链接。否则,您将看不到任何内容。

虽然如果您的应用程序只有一两个用户,这就足够了,但如果您开放注册,这还不够。


第 3 步:安装 ACL

打开用户控制器并添加 ACL 组件。

var $components = array('Auth', 'Acl');

接下来,让我们创建一个函数来安装 ACO 和 ARO 节点。

function install(){     
    if($this->Acl->Aro->findByAlias("Admin")){ 
        $this->redirect('/'); 
    } 
    $aro = new aro(); 
 
    $aro->create(); 
    $aro->save(array( 
        'model' => 'User', 
        'foreign_key' => null, 
        'parent_id' => null, 
        'alias' => 'Super' 
    )); 
 
    $aro->create(); 
    $aro->save(array( 
        'model' => 'User', 
        'foreign_key' => null, 
        'parent_id' => null, 
        'alias' => 'Admin' 
    )); 
 
    $aro->create(); 
    $aro->save(array( 
        'model' => 'User', 
        'foreign_key' => null, 
        'parent_id' => null, 
        'alias' => 'User' 
    )); 
 
    $aro->create(); 
    $aro->save(array( 
        'model' => 'User', 
        'foreign_key' => null, 
        'parent_id' => null, 
        'alias' => 'Suspended' 
    )); 
 
    $aco = new Aco(); 
    $aco->create(); 
    $aco->save(array( 
        'model' => 'User', 
        'foreign_key' => null, 
        'parent_id' => null, 
        'alias' => 'User' 
    )); 
 
    $aco->create(); 
    $aco->save(array( 
       'model' => 'Post', 
       'foreign_key' => null, 
       'parent_id' => null, 
       'alias' => 'Post' 
    )); 
 
    $this->Acl->allow('Super', 'Post', '*'); 
    $this->Acl->allow('Super', 'User', '*'); 
    $this->Acl->allow('Admin', 'Post', '*'); 
    $this->Acl->allow('User', 'Post', array('create')); 
}

通过导入ACL组件,我们可以访问ACO和ARO模型。我们首先创建一个 ARO,它是对象的请求者;换句话说,谁将访问这些对象。这就是用户(因此是模型中的用户字符串)。我们正在创造不同的角色;超级、管理员、用户和暂停。

接下来,我们对 ACO 执行相同的操作,因为我们只有两个对象要管理(帖子和用户),所以我们将创建两个对象,每个对象一个。

现在,快速说明一下。我们为 ACO 和 ARO 保存的数组有四个字段:模型名称、外键(如果您想授予对一个 ARO 的访问权限,例如他创建的帖子,则该字段很有用)、父 ID(稍后将使用)和 是基本的父子节点关系;我们将在这些角色下创建用户)和别名(这是查找对象的快速形式)。

最后,我们使用 ACL 的 allow() 函数。第一个参数是ACO别名;第二,ARO 别名,第三,授予所述 ARO 的权限。超级用户可以完全访问帖子和用户模型,管理员可以完全访问帖子模型,用户只能创建帖子。

在函数的开头,我声明了一个条件来检查 ACO 中是否存在管理员角色。您不想多次安装相同的东西,对吗?这会严重扰乱数据库。

在 Web 浏览器中打开 /users/install,由于我们没有视图,CakePHP 会抛出错误,但只需检查 MySQL 转储即可。所有关系都已成功创建,是时候处理子节点了。

将用户设置为角色

让我们清理 users 表。打开 phpMyAdmin,选择您的数据库,users 表,然后单击清空。我们将回到用户控制器上的 register() 函数。就在这一行下方:

 $this->Auth->login($data);

粘贴此代码:

// Set the user roles 
$aro = new Aro(); 
$parent = $aro->findByAlias($this->User->find('count') > 1 ? 'User' : 'Super'); 
 
$aro->create(); 
$aro->save(array( 
     'model'        => 'User', 
     'foreign_key'    => $this->User->id, 
     'parent_id'    => $parent['Aro']['id'], 
     'alias'        => 'User::'.$this->User->id 
));

在第一行中,我们创建一个新的 ARO 对象。然后我们将获取将在其中创建用户的父节点。如果数据库中有记录,我们将其设置为用户 ARO,否则,第一个用户应该是 Super。

然后我们保存一个新的ARO;该模型是我们当前正在开发的模型,Userforeign_key 是我们刚刚创建的最后一条记录的 id。 parent_id 是我们开始的节点;我们只传递 id 和最后的别名。最好将其命名为 Model_name,然后是分隔符 ::,然后是标识符。找到它会容易得多。

现在我们完成了。创建四个用户:超级用户、管理员用户、普通用户和暂停用户。我建议使用相同的用户名和密码进行测试。不要忘记,到目前为止,只有超级用户具有 Super 角色;剩下的都是用户!


第4步:阅读权限

由于 ACL 是一个组件,因此只能在控制器内访问它。稍后,它也会出现在视野中;但首先要紧的事情。将 ACL 组件包含在 Posts 控制器中,就像我们在 Users 控制器中所做的那样。现在您可以开始检查权限。让我们进入编辑功能并进行快速测试。在该方法的第一行添加此内容。

$user = $this->Auth->user(); 
if(!$this->Acl->check('User::'.$user['User']['id'], 'Post', 'update')) die('you are not authorized');

ACL 的 check() 函数需要三个参数:ARO、ACO 和操作。去编辑帖子,如果您没有以超级用户身份登录,脚本就会死掉。转到 /users/login 并以超级用户身份访问并返回编辑。您应该能够编辑该帖子。检查下面的 MySQL 转储,看看它的神奇之处。四个数据库查询:这肯定是不可扩展的。

我们有两个问题。首先,两行用于权限检查。其次,ACL 没有被缓存。并且不要忘记所有登录用户都可以看到编辑链接,即使只有超级用户可以使用它。

让我们创建一个新组件。我们称之为 Access。

<?php 
class AccessComponent extends Object{ 
    var $components = array('Acl', 'Auth'); 
    var $user; 
 
    function startup(){ 
        $this->user = $this->Auth->user(); 
    } 
} 
?>

将其保存在controllers/components中,命名为access.php。该类的 $user var 将在组件加载时启动,而 startup() 是一个回调函数,因此现在在该类中设置。现在让我们创建 check() 函数。

function check($aco, $action='*'){ 
    if(!empty($this->user) && $this->Acl->check('User::'.$this->user['User']['id'], $aco, $action)){ 
        return true; 
    } else { 
        return false; 
    } 
}

我们的 check() 方法只需要两个参数:ACO 和操作(可选)。 ARO 将是每个会话的当前用户。操作参数默认为 *,这是 ARO 的完全访问权限。该函数首先检查 $this->user 是否不为空(这实际上告诉我们用户是否已登录),然后转到 ACL。我们已经介绍过这一点。

我们现在可以将 Access 组件包含在我们的 Posts 控制器中,并只需一行即可检查权限。

if(!$this->Access->check('Post', 'update')) die('you are not authorized');

用更少的代码可以达到相同的结果,但错误消息很难看。您最好将 die() 替换为 CakePHP 错误处理程序:

$this->cakeError('error404');

视图中的权限

这可能很难看,但它确实有效。我们必须创建一个帮助程序,使用自定义方法加载组件以供在帮助程序中使用。

在Access组件(controllers/components/access.php)中添加此函数。

function checkHelper($aro, $aco, $action = "*"){ 
    App::import('Component', 'Acl'); 
    $acl = new AclComponent(); 
    return $acl->check($aro, $aco, $action); 
}

现在,让我们重写访问助手(views/helpers/access.php)。

<?php 
class AccessHelper extends Helper{ 
    var $helpers = array("Session"); 
    var $Access; 
    var $Auth; 
    var $user; 
 
    function beforeRender(){ 
        App::import('Component', 'Access'); 
        $this->Access = new AccessComponent(); 
 
        App::import('Component', 'Auth'); 
        $this->Auth = new AuthComponent(); 
        $this->Auth->Session = $this->Session; 
 
        $this->user = $this->Auth->user(); 
    } 
 
    function check($aco, $action='*'){ 
        if(empty($this->user)) return false; 
        return $this->Access->checkHelper('User::'.$this->user['User']['id'], $aco, $action); 
    } 
 
    function isLoggedin(){ 
        return !empty($this->user); 
    } 
} 
?>

beforeRender()方法是一个回调,类似于组件的startup()。我们正在加载两个组件,由于它们将在大多数函数中使用,因此最好一次启动所有组件,而不是每次调用方法时手动启动它们。

现在,在 views/posts 中的 index.ctp 视图中,您可以替换此行。

<small><a href="/posts/edit/<? echo $post['Post']['id'] ?>">edit</a> | <? echo $html->link('delete', '/posts/delete/'.$post['Post']['id'], NULL, 'Are you sure?'); ?></small>

有了这个。

<? if($access->check('Post')): ?><small><a href="/posts/edit/<? echo $post['Post']['id'] ?>">edit</a> | <? echo $html->link('delete', '/posts/delete/'.$post['Post']['id'], NULL, 'Are you sure?'); ?></small><? endif; ?>

只是不要忘记检查视图和控制器中的权限!

用户数据

您可以使用 Auth 组件中的 user() 方法访问已登录用户的用户数据。然后您可以访问该数组并获取您想要的信息。但一定有更好的方法。让我们在 Access 组件中添加以下函数。

function getmy($what){ 
    return !empty($this->user) && isset($this->user['User'][$what]) ? $this->user['User'][$what] : false; 
}

当您需要保存具有 user_id 关系的帖子时,这非常有用。

$this->data['Post']['user_id'] = $this->Access->getmy('id');

在视图中,我们可以使用助手做类似的事情。

function getmy($what){ 
    return !empty($this->user) && isset($this->user['User'][$what]) ? $this->user['User'][$what] : false; 
}

在模板文件中,您可以执行如下操作,通过用户名向用户打招呼。

Welcome <?=$access->isLoggedIn() ? $access->getmy('username') : 'Guest'; ?>

第5步:修改权限

假设我们需要为用户 #4 切换角色:他需要成为超级用户。因此,User 的 id 是 4,Aro 的 id 是 1。

$user_id = 4; 
$user_new_group = 1;

现在我们需要找到用户的 Aro 以便修改其父 Aro。

$aro_user =  $this->Acl->Aro->find('first', 
    array( 
        'conditions' => array( 
            'Aro.parent_id !=' => NULL, 
            'Aro.model' => 'User', 
            'Aro.foreign_key' => $user_id 
        ) 
    ) 
);

最后,如果 $aro_user 变量不为空,让我们更新 Aro.parent_id 字段。

if(!empty($aro_user)){ 
    $data['id'] = $aro_user['Aro']['id']; 
    $data['parent_id'] = $user_new_group; 
    $this->Acl->Aro->save($data); 
}

请注意,您必须验证用户的 ID 和新 aro 的 ID。如果其中之一不存在,则会在您的 ACL 表中造成混乱。


第 6 步:优化

虽然我们刚刚创建的组件既简单又有用,但每次检查都会查询数据库。目前尚未准备好投入生产。首先,应尽可能少地查询数据库。为了优化这一点,我们应该利用 CakePHP 的缓存。

使用缓存会大大减少负载,但如果我们有十个帖子出现在索引中,并且对于每个帖子,我们都会检查用户是否具有帖子 Aco 的更新权限,框架将读取并解析一个文件返回相同的结果...每个页面加载十次。

这是第二点:类内的变量和一些条件将使工作更轻松,以便重复的请求将仅检查 Cache 一次。

这两个更改都反映在 access_cache.php 中,位于 controllers/components 目录中。因此,请确保下载源代码!


结论

访问控制列表是大多数应用程序需要的基本功能。 CakePHP 有一个很好的实现,但缺乏良好的文档和示例。我希望通过本教程能够解决这两个问题。感谢您的阅读!

卓越飞翔博客
上一篇: 如何利用C++进行高性能的图像检索和图像分类?
下一篇: 如何设计出可扩展的C++应用架构?
留言与评论(共有 0 条评论)
   
验证码:
隐藏边栏