访问控制列表 (ACL)¶
概览¶
The Phalcon\Acl组件提供了一种轻量且直接的方法来管理访问控制列表 (ACL) 及其相关权限。ACL 在控制应用程序中各个区域及底层对象的访问方面起着至关重要的作用。
在编程中,ACL 通常涉及两个关键实体:请求访问的对象(角色)和被访问的对象(组件或资源)。对于 Phalcon 来说,它们被称为角色和组件。看一个实际场景,角色定义用户组,而组件表示应用程序的各个区域。
使用案例
会计应用程序需要让不同用户组能够访问应用程序的不同区域。
角色
-  
管理员访问权限
 -  
会计部门访问权限
 -  
管理员访问权限
 -  
游客访问权限
 
组件
-  
登录页面
 -  
管理页面
 -  
发票页面
 -  
报告页面
 
在此示例中,角色表示谁需要访问特定的组件中。一个组件表示应用程序的一个区域。使用Phalcon\Acl组件,您可以建立这些角色和组件的关联,通过只允许特定角色访问指定组件来增强应用程序的安全性。
激活¶
Phalcon\Acl依赖适配器来管理角色和组件。目前唯一可用的适配器是Phalcon\Acl\Adapter\Memory。虽然使用内存适配器可以显著提升 ACL 的访问速度,但它也有非持久性的缺点。因此,开发者需要为 ACL 数据实现一种存储策略,以避免每次请求时重新生成 ACL。这对于存储在数据库或文件系统中的大型 ACL 尤为重要。
The Phalcon\Acl构造函数的第一个参数是一个适配器,用于检索与控制列表相关的信息。
默认操作是Phalcon\Acl\Enum::DENY对于任何角色或组件。这种默认设置确保只有开发者或应用程序明确允许访问特定组件,而不是 ACL 组件本身。
<?php
use Phalcon\Acl\Enum;
use Phalcon\Acl\Adapter\Memory;
$acl = new Memory();
$acl->setDefaultAction(Enum::ALLOW);
常量¶
The Phalcon\Acl\Enum类提供了两个常量用于定义访问级别:
Phalcon\Acl\Enum::ALLOW(1)Phalcon\Acl\Enum::DENY(0– 默认)
这些常量有助于在您的 ACL 中指定访问级别。
添加角色¶
Phalcon\Acl\Roles表示可以在或不能在 ACL 中访问一组组件的对象。有两种添加角色的方法:
- 使用一个Phalcon\Acl\Role对象
 - 使用一个字符串表示角色名称
 
在下面的示例中,将用例中提到的角色添加到 ACL 中:
使用Phalcon\Acl\Role对象:
<?php
use Phalcon\Acl\Adapter\Memory;
use Phalcon\Acl\Role;
$acl = new Memory();
$roleAdmins     = new Role('admins', 'Administrator Access');
$roleAccounting = new Role('accounting', 'Accounting Department Access'); 
$acl->addRole($roleAdmins);
$acl->addRole($roleAccounting);
使用字符串:
<?php
use Phalcon\Acl\Adapter\Memory;
$acl = new Memory();
$acl->addRole('manager');
$acl->addRole('guest');
添加组件¶
A 组件在 Phalcon\Acl 上下文中代表应用程序中受控访问的区域。在 MVC 应用程序中,这通常对应一个控制器。虽然不是强制性的,但您可以使用Phalcon\Acl\Component类来定义应用程序中的组件。向组件添加相关动作非常重要,以便 ACL 能够明白它应该控制什么。
有以下两种方式将组件添加到我们的列表中:
- 使用Phalcon\Acl\Component对象中的相关方法手动
 - 使用一个字符串表示组件名称。
 
类似于addRole方法中,addComponent 方法需要一个组件名称以及一个可选描述。
组件对象:¶
<?php
use Phalcon\Acl\Adapter\Memory;
use Phalcon\Acl\Component;
$acl = new Memory();
$admin   = new Component('admin', 'Administration Pages');
$reports = new Component('reports', 'Reports Pages');
$acl->addComponent(
    $admin,
    [
        'dashboard',
        'users',
    ]
);
$acl->addComponent(
    $reports,
    [
        'list',
        'add',
    ]
);
字符串:¶
<?php
use Phalcon\Acl\Adapter\Memory;
$acl = new Memory();
$acl->addComponent(
    'admin',
    [
        'dashboard',
        'users',
    ]
);
$acl->addComponent(
    'reports',
    [
        'list',
        'add',
    ]
);
定义访问控制¶
在定义了Roles和Components之后,下一步是将它们结合起来创建访问列表。这是一个关键步骤,因为此处的小错误可能无意中允许某些角色访问开发者本不希望他们访问的组件。如前所述,Phalcon\Acl是Phalcon\Acl\Enum::DENY的默认访问操作遵循白名单方法。
要关联角色和组件,请使用allow()和deny()提供的方法。Phalcon\Acl\Memory类。
示例:
<?php
use Phalcon\Acl\Adapter\Memory;
use Phalcon\Acl\Role;
use Phalcon\Acl\Component;
$acl = new Memory();
$acl->addRole('manager');
$acl->addRole('accounting');
$acl->addRole('guest');
$acl->addComponent(
    'admin',
    [
        'dashboard',
        'users',
        'view',
    ]
);
$acl->addComponent(
    'reports',
    [
        'list',
        'add',
        'view',
    ]
);
$acl->addComponent(
    'session',
    [
        'login',
        'logout',
    ]
);
$acl->allow('manager', 'admin', 'dashboard');
$acl->allow('manager', 'reports', ['list', 'add']);
$acl->allow('accounting', 'reports', '*');
$acl->allow('*', 'session', '*');
在上面的例子中:
$acl->allow('manager', 'admin', 'dashboard');:对于manager角色,允许访问admin组件和dashboard动作。在 MVC 术语中,这允许manager角色访问admin控制器和dashboard动作。$acl->allow('manager', 'reports', ['list', 'add']);:调用action参数传递。此行的意思是,对于allow()方法时可以将数组作为manager角色,允许访问reports组件和list和add所有动作。在 MVC 术语中,这意味着允许manager角色访问reports控制器和list和add所有动作。$acl->allow('*', 'session', '*');:通配符可用于批量匹配角色、组件或动作。这一行允许所有角色访问session组件定义的。$acl->allow('*', '*', 'view');:这一行允许所有角色访问view动作。在 MVC 术语中,这意味着任何角色都可以访问暴露了viewAction.$acl->deny('guest', '*', 'view');:对于guest角色,拒绝访问所有具有view动作的组件。尽管默认访问级别是Acl\Enum::DENY,该行专门拒绝了所有角色和组件对view动作的访问。这确保了guest角色只能访问session组件和login和logout动作,因为游客没有登录应用程序。$acl->allow('*', '*', 'view');:这一行允许所有角色访问view动作。然而,下一行排除了guest角色对此的访问:
注意
请注意非常谨慎地使用*通配符。很容易犯错误,虽然通配符看似方便,但它可能会让用户访问他们不应访问的应用程序区域。最稳妥的方式是编写专门测试权限和 ACL 的测试用例。这些测试可以在unit通过实例化组件然后检查来创建测试套件。isAllowed()如果它是true或false.
在我们的 GitHub 仓库(https://github.com/phalcon/cphalcon)中的tests文件夹里包含大量测试用例,可以为你提供指导和灵感。
查询¶
一旦列表被定义,你可以查询它以使用 isAllowed() 方法检查特定角色是否具有访问特定组件和操作的权限。
示例:
<?php
use Phalcon\Acl\Adapter\Memory;
use Phalcon\Acl\Role;
use Phalcon\Acl\Component;
$acl = new Memory();
// (Roles and Components setup...)
// Check permissions
$acl->isAllowed('manager', 'admin', 'dashboard'); // true – explicitly defined
$acl->isAllowed('manager', 'session', 'login');   // true – defined with wildcard
$acl->isAllowed('accounting', 'reports', 'view'); // true – defined with wildcard
$acl->isAllowed('guest', 'reports', 'view');      // false – explicitly defined
$acl->isAllowed('guest', 'reports', 'add');       // false – default access level
在上面的例子中,将isAllowed()方法用于检查某个角色是否有权限访问特定的组件和动作。它返回true如果允许访问,false否则返回 false。该方法对于在应用程序中实现基于角色的访问控制非常有用。
基于函数的访问¶
根据你的应用程序需求,你可能需要额外一层计算逻辑,以通过 ACL 允许或拒绝用户访问。Phalcon 的 ACL 中的isAllowed()方法接受第四个参数,这是一个callable如匿名函数。为了利用这个功能,在为指定的角色和组件调用allow()方法时,你需要定义你的函数。例如,假设你需要允许所有manager角色访问admin组件,除非其名称是 'Bob'。要实现这一点,你可以注册一个检查此条件的匿名函数。
示例:
<?php
use Phalcon\Acl\Adapter\Memory;
use Phalcon\Acl\Role;
use Phalcon\Acl\Component;
$acl = new Memory();
// Add roles
$acl->addRole('manager');
// Add components
$acl->addComponent(
    'admin',
    [
        'dashboard',
        'users',
        'view',
    ]
);
// Set access level for `role` into `components` with a custom function
$acl->allow(
    'manager',
    'admin',
    'dashboard',
    function ($name) {
        return boolval('Bob' !== $name);
    }
);
现在 ACL 中已经定义了可调用对象,你需要调用isAllowed()方法并传入一个数组作为第四个参数:
示例:
<?php
use Phalcon\Acl\Adapter\Memory;
use Phalcon\Acl\Role;
use Phalcon\Acl\Component;
$acl = new Memory();
// Add roles
$acl->addRole('manager');
// Add components
$acl->addComponent(
    'admin',
    [
        'dashboard',
        'users',
        'view',
    ]
);
// Set access level for `role` into `components` with a custom function
$acl->allow(
    'manager',
    'admin',
    'dashboard',
    function ($name) {
        return boolval('Bob' !== $name);
    }
);
// Returns `true`
$acl->isAllowed(
    'manager',
    'admin',
    'dashboard',
    [
        'name' => 'John',
    ]
);
// Returns `false`
$acl->isAllowed(
    'manager',
    'admin',
    'dashboard',
    [
        'name' => 'Bob',
    ]
);
注意
第四个参数必须是一个数组。每个数组元素代表你的匿名函数接受的一个参数。元素的键是参数名称,值则是将传递给函数的该参数的值。
你也可以选择不向isAllowed()方法传递第四个参数。如果调用isAllowed()方法时不传递最后一个参数,默认行为是Acl\Enum::DENYfalse。如果你想改变这种行为,可以调用setNoArgumentsDefaultAction():
示例:
<?php
use Phalcon\Acl\Enum;
use Phalcon\Acl\Adapter\Memory;
use Phalcon\Acl\Role;
use Phalcon\Acl\Component;
$acl = new Memory();
// Add roles
$acl->addRole('manager');
// Add components
$acl->addComponent(
    'admin',
    [
        'dashboard',
        'users',
        'view',
    ]
);
// Set access level for `role` into `components` with a custom function
$acl->allow(
    'manager',
    'admin',
    'dashboard',
    function ($name) {
        return boolval('Bob' !== $name);
    }
);
// Returns `false`
$acl->isAllowed('manager', 'admin', 'dashboard');
$acl->setNoArgumentsDefaultAction(
    Enum::ALLOW
);
// Returns `true`
$acl->isAllowed('manager', 'admin', 'dashboard');
自定义对象¶
Phalcon 允许开发者定义自己的角色和组件对象。这些对象必须实现提供的接口:
角色¶
你可以在自定义类中实现Phalcon\Acl\RoleAwareInterface接口,并加入你自己的逻辑。下面的例子展示了一个名为ManagerRole:
<?php
use Phalcon\Acl\RoleAwareInterface;
// Create our class, which will be used as roleName
class ManagerRole implements RoleAwareInterface
{
    protected $id;
    protected $roleName;
    public function __construct($id, $roleName)
    {
        $this->id = $id;
        $this->roleName = $roleName;
    }
    public function getId()
    {
        return $this->id;
    }
    // Implemented function from RoleAware Interface
    public function getRoleName()
    {
        return $this->roleName;
    }
}
组件¶
你可以在自定义类中实现Phalcon\Acl\ComponentAwareInterface接口,并加入你自己的逻辑。下面的例子展示了一个名为ReportsComponent:
<?php
use Phalcon\Acl\ComponentAwareInterface;
// Create our class, which will be used as componentName
class ReportsComponent implements ComponentAwareInterface
{
    protected $id;
    protected $componentName;
    protected $userId;
    public function __construct($id, $componentName, $userId)
    {
        $this->id = $id;
        $this->componentName = $componentName;
        $this->userId = $userId;
    }
    public function getId()
    {
        return $this->id;
    }
    public function getUserId()
    {
        return $this->userId;
    }
    // Implemented function from ComponentAware Interface
    public function getComponentName()
    {
        return $this->componentName;
    }
}
ACL¶
这些对象现在可以在你的 ACL 中使用。
<?php
use ManagerRole;
use Phalcon\Acl\Adapter\Memory;
use Phalcon\Acl\Role;
use Phalcon\Acl\Component;
use ReportsComponent;
$acl = new Memory();
// Add roles
$acl->addRole('manager');
// Add components
$acl->addComponent(
    'reports',
    [
        'list',
        'add',
        'view',
    ]
);
// Now tie them all together with a custom function.
// The `ManagerRole` and `ModelSubject` parameters are necessary
// for the custom function to work
$acl->allow(
    'manager', 
    'reports', 
    'list',
    function (ManagerRole $manager, ReportsComponent $model) {
        return boolval($manager->getId() === $model->getUserId());
    }
);
// Create the custom objects
$levelOne = new ManagerRole(1, 'manager-1');
$levelTwo = new ManagerRole(2, 'manager');
$admin    = new ManagerRole(3, 'manager');
// id – name – userId
$reports  = new ReportsComponent(2, 'reports', 2);
// Check whether our user objects have access. Returns `false`
$acl->isAllowed($levelOne, $reports, 'list');
// Returns `true`
$acl->isAllowed($levelTwo, $reports, 'list');
// Returns `false`
$acl->isAllowed($admin, $reports, 'list');
第二次对$levelTwo的调用进行评估true因为getUserId()返回2这会进一步在我们的自定义函数中进行评估。同时请注意,在allow()的自定义函数中,对象会自动绑定,提供了自定义函数运行所需的全部数据。自定义函数可以接受任意数量的附加参数。在function()构造函数中定义的参数顺序并不重要,因为对象会自动发现并绑定。
角色继承¶
为了减少重复并提高应用程序的效率,ACL 提供了角色继承功能。这意味着你可以先定义一个Phalcon\Acl\Role作为基础角色,然后从它继承,从而授予对组件的超集或子集的访问权限。要使用角色继承,当你将角色添加到列表时,需将被继承的角色作为方法调用的第二个参数传递。
示例:
<?php
use Phalcon\Acl\Adapter\Memory;
use Phalcon\Acl\Role;
$acl = new Memory();
// Create roles
$manager    = new Role('Managers');
$accounting = new Role('Accounting Department');
$guest      = new Role('Guests');
// Add the `guest` role to the ACL
$acl->addRole($guest);
// Add the `accounting` role inheriting from `guest`
$acl->addRole($accounting, $guest);
// Add the `manager` role inheriting from `accounting`
$acl->addRole($manager, $accounting);
无论guests拥有什么样的访问权限,accounting都会继承这些权限,accounting然后又会把权限传播给manager。你还可以将一组角色作为addRole方法调用的第二个参数传入,这样能提供更多灵活性。
角色关系¶
根据你的应用程序设计,你可能更倾向于先添加所有角色,然后再定义它们之间的关系。
示例:
<?php
use Phalcon\Acl\Adapter\Memory;
use Phalcon\Acl\Role;
$acl = new Memory();
// Create roles
$manager    = new Role('Managers');
$accounting = new Role('Accounting Department');
$guest      = new Role('Guests');
// Add all the roles
$acl->addRole($manager);
$acl->addRole($accounting);
$acl->addRole($guest);
// Add the inheritance
$acl->addInherit($manager, $accounting);
$acl->addInherit($accounting, $guest);
序列化¶
Phalcon\Acl可以序列化并存储在缓存系统中以提高效率。你可以将序列化后的对象存储在 APC、会话、文件系统、数据库、Redis 等地方。这样,你可以快速检索 ACL,而不必每次都重新读取生成 ACL 的底层数据,也不需要在每次请求时重新计算 ACL。
示例:
<?php
use Phalcon\Acl\Adapter\Memory;
$aclFile = 'app/security/acl.cache';
// Check whether ACL data already exist
if (!is_file($aclFile)) {
    // The ACL does not exist – build it
    $acl = new Memory();
    // Define roles, components, access, etc.
    // ...
    // Store serialized list into a plain file
    file_put_contents(
        $aclFile,
        serialize($acl)
    );
} else {
    // Restore the ACL object from the serialized file
    $acl = unserialize(
        file_get_contents($aclFile)
    );
}
// Use the ACL list as needed
if ($acl->isAllowed('manager', 'admin', 'dashboard')) {
    echo 'Access granted!';
} else {
    echo 'Access denied :(';
}
在开发期间避免使用 ACL 的序列化是一个好习惯,以确保你的 ACL 在每次请求时都会重建,而在生产环境中则可以使用其他适配器或方式来序列化和存储 ACL。
事件¶
Phalcon\Acl可以配合事件管理器使用(如果存在的话),以便向你的应用程序触发事件。事件使用类型acl来触发。返回false的事件可以停止当前角色的执行。以下是可用的事件:
| 事件名称 | 触发时机 | 可以停止角色? | 
|---|---|---|
afterCheckAccess |  在检查角色/组件是否有访问权限之后触发 | 否 | 
beforeCheckAccess |  在检查角色/组件是否有访问权限之前触发 | 是 | 
示例:
<?php
use Phalcon\Acl\Adapter\Memory;
use Phalcon\Events\Event;
use Phalcon\Events\Manager;
// ...
// Create an event manager
$eventsManager = new Manager();
// Attach a listener for type `acl`
$eventsManager->attach(
    'acl:beforeCheckAccess',
    function (Event $event, $acl) {
        echo $acl->getActiveRole() . PHP_EOL;
        echo $acl->getActiveComponent() . PHP_EOL;
        echo $acl->getActiveAccess() . PHP_EOL;
    }
);
$acl = new Memory();
// Setup the `$acl`
// ...
// Bind the eventsManager to the ACL component
$acl->setEventsManager($eventsManager);
异常¶
在Phalcon\Acl命名空间将是类型Phalcon\Acl\Exception。你可以使用此异常选择性地捕获仅从此组件抛出的异常。
示例:
<?php
use Phalcon\Acl\Adapter\Memory;
use Phalcon\Acl\Component;
use Phalcon\Acl\Exception;
try {
    $acl   = new Memory();
    $admin = new Component('*');
} catch (Exception $ex) {
    echo $ex->getMessage();
}
自定义¶
The Phalcon\Acl\AdapterInterface接口必须被实现,才能创建你自己的 ACL 适配器或扩展现有的 ACL 适配器。