简单的 Acl 控制的应用

注解

这不是初学者的教程。如果你刚刚开始学习 CakePHP,我们建议你还是先对整个框架的特点全面了解之后再开始本教程。

在这个教程中,你将会使用 AuthenticationAccess Control Lists 创建一个简单的应用。这个教程假设你已经阅读过 博客教程,并且熟悉Code Generation with Bake。你应该对 CakePHP 有了一些经验, 并且熟悉 MVC 概念。这个教程是对 AuthComponentAclComponent 的一个简单介绍。

你所需要的:

  • 一个可以运行的 web 服务器。我们将假定你使用的是 Apache,不过使用其他服务器的步骤也差不多。我们也许要稍微调整服务器的配置,但大多数人完全不需要任何配置就可以让 CakePHP 运行起来。
  • 一个数据库服务器。在本教程中我们将使用 MySQL 数据库。你将会需要对 SQL 有足够的了解以便创建一个数据库:接下来就是 CakePHP 的事情了。
  • PHP 的基础知识。你使用面向对象编程越多越好,但如果你只熟悉面向过程编程也不要害怕。

准备我们的应用

首先,让我们获取一份最新的 CakePHP 的代码。

要获得最新的代码,请访问在 GitHub 上的 CakePHP 项目:https://github.com/cakephp/cakephp/tags,并下载稳定发行版。对于本教程,你需要最新的 2.0 发行版本。

你也可以使用 git 检出最新的代码:

  1. git clone git://github.com/cakephp/cakephp.git

一旦你获得了最新的 CakePHP 代码,改变分支到最新的 2.0 版本,设置配置文件database.php,修改 app/Config/core.php 文件中的 Security.salt 的值。然后我们会建立一个简单的数据库结构,作为应用程序的基础。在数据库中执行如下的 SQL 语句:

  1. CREATE TABLE users (
  2. id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
  3. username VARCHAR(255) NOT NULL UNIQUE,
  4. password CHAR(40) NOT NULL,
  5. group_id INT(11) NOT NULL,
  6. created DATETIME,
  7. modified DATETIME
  8. );
  9.  
  10.  
  11. CREATE TABLE groups (
  12. id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
  13. name VARCHAR(100) NOT NULL,
  14. created DATETIME,
  15. modified DATETIME
  16. );
  17.  
  18.  
  19. CREATE TABLE posts (
  20. id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
  21. user_id INT(11) NOT NULL,
  22. title VARCHAR(255) NOT NULL,
  23. body TEXT,
  24. created DATETIME,
  25. modified DATETIME
  26. );
  27.  
  28. CREATE TABLE widgets (
  29. id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
  30. name VARCHAR(100) NOT NULL,
  31. part_no VARCHAR(12),
  32. quantity INT(11)
  33. );

这些是我们构建应用程序的其余部分要用到的表。一旦数据库中有了表结构,我们就可以开始了。使用 Code Generation with Bake 快速创建你的模型、控制器和视图。

要使用 cake bake,调用 cake bake all,这会列出你插入到 MySQL 中的4个表,选择"1. Group" 并按照提示操作。对其他 3 个表也进行同样的操作,这会为你生成 4 个控制器、模型和相应的视图。

在这里要避免使用脚手架(Scaffold)。如果生成带有脚手架功能的控制器,将会严重影响ACO(Access Control Object)的生成。

当自动生成模型代码时,cake 会自动探测出模型之间的关联(即表之间的关系)。让 cake提供正确的 hasMany 和 belongsTo 关系。如果提示你选择 hasOne 或者 hasMany 关系,在本教程中通常(只)需要 hasMany 关系。

现在先不管 admin 路由,没有它们这个话题已经够复杂的了。另外,在用 bake 生成控制器时,一定 不要 添加 Acl 或者 Auth 组件到任何控制器中。我们很快就会着手于此。你现在应该已经有了 users、groups、posts 和 widgets 的模型、控制器以及生成的视图。

准备添加 Auth

我们现在已经有一个运行正常的 CRUD 应用了。Bake 应该已经建立了我们所需要的关系,如果没有,现在就加上。在添加 Auth 和 Acl 组件之前,我们还需要添加一些东西。首先,添加 login 和 logout 动作到 UsersController 控制器:

  1. public function login() {
  2. if ($this->request->is('post')) {
  3. if ($this->Auth->login()) {
  4. return $this->redirect($this->Auth->redirectUrl());
  5. }
  6. $this->Session->setFlash(__('Your username or password was incorrect.'));
  7. }
  8. }
  9.  
  10. public function logout() {
  11. //现在先空着。
  12. }

然后,为 login 动作创建如下所示的视图文件 app/View/Users/login.ctp:

  1. <?php
  2. echo $this->Form->create('User', array('action' => 'login'));
  3. echo $this->Form->inputs(array(
  4. 'legend' => __('Login'),
  5. 'username',
  6. 'password'
  7. ));
  8. echo $this->Form->end('Login');
  9. ?>

接下来,我们需要更新我们的 User 模型,在保存到数据库之前先将密码散列化。存储普通文本格式的密码是极其危险的,并且 AuthComponent 组件会期望你的密码是经过散列化过的。在 app/Model/User.php 文件中添加如下代码:

  1. App::uses('AuthComponent', 'Controller/Component');
  2. class User extends AppModel {
  3. // 其它代码。
  4.  
  5. public function beforeSave($options = array()) {
  6. $this->data['User']['password'] = AuthComponent::password(
  7. $this->data['User']['password']
  8. );
  9. return true;
  10. }
  11. }

接下来要改动一下 AppController。如果还没有/app/Controller/AppController.php,就创建该文件。因为我们要使用 Auth 和 Acl组件控制整个网站,所以我们会在 AppController 中把它们设置好:

  1. class AppController extends Controller {
  2. public $components = array(
  3. 'Acl',
  4. 'Auth' => array(
  5. 'authorize' => array(
  6. 'Actions' => array('actionPath' => 'controllers')
  7. )
  8. ),
  9. 'Session'
  10. );
  11. public $helpers = array('Html', 'Form', 'Session');
  12.  
  13. public function beforeFilter() {
  14. //配置 AuthComponent 组件
  15. $this->Auth->loginAction = array(
  16. 'controller' => 'users',
  17. 'action' => 'login'
  18. );
  19. $this->Auth->logoutRedirect = array(
  20. 'controller' => 'users',
  21. 'action' => 'login'
  22. );
  23. $this->Auth->loginRedirect = array(
  24. 'controller' => 'posts',
  25. 'action' => 'add'
  26. );
  27. }
  28. }

在设置 ACL 组件之前,需要添加一些用户和组。因为启用了 AuthComponent组件,我们无法访问任何动作,因为还没有登录。现在我们添加一些特例,这样AuthComponent 组件就会允许我们创建一些组和用户。在GroupsController 控制器和 UsersController 控制器中 添加:

  1. public function beforeFilter() {
  2. parent::beforeFilter();
  3.  
  4. // 对 CakePHP 2.0
  5. $this->Auth->allow('*');
  6.  
  7. // 对 CakePHP 2.1 及以上版本
  8. $this->Auth->allow();
  9. }

这些语句告诉 AuthComponent 组件,允许公开访问所有动作。这只是临时的,一旦我们在数据库中有了一些用户和组之后就会去掉。只是现在还不要添加任何用户或组。

初始化 Db Acl 表

在我们创建任何用户或者组之前,我们要把它们连接到 Acl 组件。不过,我们现在还没有任何 Acl 组件的表,如果你现在试图访问任何页面,你可能会得到表不存在的错误("Error: Database table acos for model Aco was not found.")。要消除这些错误,我们需要运行一个数据结构(schema)文件。在命令行执行下面的命令:

  1. ./Console/cake schema create DbAcl

这个脚本会提示你删除并新建表。对删除和创建表的请求回答 yes。

如果你没有访问外壳(shell)的权限,或者无法使用终端,你可以执行 sql 文件/path/to/app/Config/Schema/db_acl.sql。

为数据输入设置了控制器,也初始化了 Acl 组件的表,这就行了吗?还不够,还需要在用户(user)和组(group)模型中稍做改动,也就是说,让他们自动地附加上 Acl 组件。

作为请求者

为了让 Auth 组件和 Acl 组件正常工作,我们需要将用户(users)表和组(groups)表同Acl 组件的表中的记录进行关联。为此需要用到 AclBehavior 行为。AclBehavior允许将模型自动连接到 Acl 组件的表。使用它需要在模型中实现 parentNode() 方法。在 User 模型中添加如下代码:

  1. class User extends AppModel {
  2. public $belongsTo = array('Group');
  3. public $actsAs = array('Acl' => array('type' => 'requester'));
  4.  
  5. public function parentNode() {
  6. if (!$this->id && empty($this->data)) {
  7. return null;
  8. }
  9. if (isset($this->data['User']['group_id'])) {
  10. $groupId = $this->data['User']['group_id'];
  11. } else {
  12. $groupId = $this->field('group_id');
  13. }
  14. if (!$groupId) {
  15. return null;
  16. }
  17. return array('Group' => array('id' => $groupId));
  18. }
  19. }

然后在 Group 模型中添加如下代码:

  1. class Group extends AppModel {
  2. public $actsAs = array('Acl' => array('type' => 'requester'));
  3.  
  4. public function parentNode() {
  5. return null;
  6. }
  7. }

我们所做的,就是将 GroupUser 模型与 Acl 组件联系起来,并告诉 CakePHP每次你创建一个用户(User)或组(Group)的同时也要在 aros 表中创建一条记录。这使得 Acl 的管理轻而易举,因为 ARO 透明地与 usersgroups 表绑定在一起了。所以,每次创建或者删除一个用户/组的同时,Aro 表也会更新。

我们的控制器和模型已经可以添加一些初始数据了,而且我们的 GroupUser模型已经绑定到 Acl 组件的表了。所以可以浏览 http://example.com/groups/addhttp://example.com/users/add,使用自动生成的表单添加一些组和用户。我添加了这些组:

  • administrators
  • managers
  • users
    我同时也在每个组中创建了一个用户,这样每个不同访问权限组都有一个用户,用于之后的测试。(把这些组和用户)全部记录下来,或者选用简单的密码,以免忘记。如果在 MySQL提示符后运行 SELECT * FROM aros;,应该可以看到象下面这样的记录:
  1. +----+-----------+-------+-------------+-------+------+------+
  2. | id | parent_id | model | foreign_key | alias | lft | rght |
  3. +----+-----------+-------+-------------+-------+------+------+
  4. | 1 | NULL | Group | 1 | NULL | 1 | 4 |
  5. | 2 | NULL | Group | 2 | NULL | 5 | 8 |
  6. | 3 | NULL | Group | 3 | NULL | 9 | 12 |
  7. | 4 | 1 | User | 1 | NULL | 2 | 3 |
  8. | 5 | 2 | User | 2 | NULL | 6 | 7 |
  9. | 6 | 3 | User | 3 | NULL | 10 | 11 |
  10. +----+-----------+-------+-------------+-------+------+------+
  11. 6 rows in set (0.00 sec)

这告诉我们已经有了 3 个组和 3 个用户。用户嵌套在组中,这样我们就可以按组或者按用户设置权限。

只按组的 ACL

如果我们要简单一些,只按组设置的权限,需要在 User 模型中实现 bindNode()方法:

  1. public function bindNode($user) {
  2. return array('model' => 'Group', 'foreign_key' => $user['User']['group_id']);
  3. }

然后修改 User 模型的 actsAs 变量,禁用 requester 指令:

  1. public $actsAs = array('Acl' => array('type' => 'requester', 'enabled' => false));

这两处改动会告诉 ACL 忽略检查 User Aro,而只检查 Group Aro's。这样也避免了对 afterSave 回调的调用。

注意:每个用户都需要设置 group_id 才行。

现在 aros 表会是这样:

  1. +----+-----------+-------+-------------+-------+------+------+
  2. | id | parent_id | model | foreign_key | alias | lft | rght |
  3. +----+-----------+-------+-------------+-------+------+------+
  4. | 1 | NULL | Group | 1 | NULL | 1 | 2 |
  5. | 2 | NULL | Group | 2 | NULL | 3 | 4 |
  6. | 3 | NULL | Group | 3 | NULL | 5 | 6 |
  7. +----+-----------+-------+-------------+-------+------+------+
  8. 3 rows in set (0.00 sec)

注意:如果你到这里一直跟随此教程,你需要删除你的表,包括 arosgroupsusers,然后从头重新创建组和用户,才能得到上面的 aros 表。

创建 ACO (Access Control Objects)

现在我们已经有了用户和组(aro),我们可以开始把现有的控制器输入到 Acl 中,并对组和用户设置权限,并启用登录/登出。

我们的 ARO 会在新建户和组的时候自动创建。有没有什么办法从控制器和动作来自动创建ACO?可惜 CakePHP 的核心没有这样的魔法。不过核心类提供了一些方法来手动创建 ACO。你可以通过 Acl 外壳程序或者 AclComponent 组件创建 ACO。从外壳程序创建 Aco:

  1. ./Console/cake acl create aco root controllers

而使用 AclComponent 组件就是:

  1. $this->Acl->Aco->create(array('parent_id' => null, 'alias' => 'controllers'));
  2. $this->Acl->Aco->save();

上面两个例子都会创建 'root' 或者顶层 ACO,会叫做 'controllers' 。这个根(root)节点的目的,是为了在整个应用程序的范围内更容易地允许/拒绝访问,并且允许把 Acl组件用于和控制器/动作无关的目的,比如检查模型记录的访问权限。既然我们要使用全局的根(root) ACO,我们要略微修改 AuthComponent 组件的配置。AuthComponent组件需要知道这个根节点的存在,这样当进行 ACL 检查的时候,它可以在查找控制器/动作时使用正确的节点路径。在 AppController 中确保 $components 数组中包含先前定义的 actionPath:

  1. class AppController extends Controller {
  2. public $components = array(
  3. 'Acl',
  4. 'Auth' => array(
  5. 'authorize' => array(
  6. 'Actions' => array('actionPath' => 'controllers')
  7. )
  8. ),
  9. 'Session'
  10. );

本教程在 简单的 Acl 控制的应用 - 第 2 部分 中继续。