Phalcon 权限控制 ACL

Access Control Lists(ACL)

Phalcon\Acl提供了一个简单和轻量的权限控制管理, 访问控制列表(ACL Access Control List)允许应用控制客户端的请求(request)对它资源(区域)的访问。建议你了解更多关于ACL的相关知识,以便熟悉我们接下来讲到的一些概念.

总体来说,ACLs句含 roles 和 resources. Resources为ACLs定义了需要访问权限的对像. Roles则是ACL对请求访问资源允许还是拒绝的对像.

Create an ACL

这个组件最初的设计是在内存中工作。考虑的是易于使用和对访问列表的访问速度上。以下是一个使用 memory adapter的例子

<?php

use Phalcon\Acl\Adapter\Memory as AclList;

$acl = new AclList();

默认的,如果没有定义, Phalcon\Acl允许资源中对像中的action。为了提高访问控制列表的安全级别,我们应该设置”deny”级别

<?php

use Phalcon\Acl;

// Default action is deny access
$acl->setDefaultAction(
    Acl::DENY
);

Adding Roles to the ACL

role是一个对像(字段只有名称和描述),它可以访问或者不能访问access list 中的某些资源。举一个例子, 我们将定义按照一个组织(公司)的人员结构来定义角色. Phalcon\Acl\Role类可以更结构化(固定字段)的方式来创建角色(也可以直接定义数组).

<?php

use Phalcon\Acl\Role;

// Create some roles.
// The first parameter is the name, the second parameter is an optional description.
$roleAdmins = new Role("Administrators", "Super-User role");
$roleGuests = new Role("Guests");

// Add "Guests" role to ACL
$acl->addRole($roleGuests);

// Add "Designers" role to ACL without a Phalcon\Acl\Role
$acl->addRole("Designers");

Adding Resources

Resource是可以控制访问的对像(字段只有名称和描述)。正常的,MVC应用程序各,Resources指的是contrllers. 但这不是强制性的,我们可以通过Phalcon\Acl\Resource类用来定义资源。在这里,最重要的是将相关的controller中的action或者操作添加到resource. 这样ACL才能知道,哪些资源需要进行控制.

<?php

use Phalcon\Acl\Resource;

// Define the "Customers" resource
$customersResource = new Resource("Customers");

// Add "customers" resource with a couple of operations

$acl->addResource(
    $customersResource,
    "search"
);

$acl->addResource(
    $customersResource,
    [
        "create",
        "update",
    ]
);

Defining Access Controls

现在我们拥有了roles和resource, 就可以定义ACL了(哪些角色可以访问哪些资源). 这个部分非常重要,特别在考虑在默认的访问级别(我们上面设置的setDefaultAction)

<?php

// Set access level for roles into resources

$acl->allow("Guests", "Customers", "search");

$acl->allow("Guests", "Customers", "create");

$acl->deny("Guests", "Customers", "update");

allow() 方法授权相关角色可以访问资源,deny()则相反

Querying an ACL

一旦访问控制列表定义完成。我们可以查询它,以确认角色是事有相关的权限

<?php

// Check whether role has access to the operations

// Returns 0
$acl->isAllowed("Guests", "Customers", "edit");

// Returns 1
$acl->isAllowed("Guests", "Customers", "search");

// Returns 1
$acl->isAllowed("Guests", "Customers", "create");

Function based access

你可以在allow()和deny()方法的第4个参数中添加自定义函数,但它必须返回一个boolean值。这个函数将在使用isAllowed()方法时。你可以在isAllowed()的第4个参数中传递一个数组,这个数组作为定义函数的参数,数组中的key作为自定义函数的参数名。

<?php
// Set access level for role into resources with custom function
$acl->allow(
    "Guests",
    "Customers",
    "search",
    function ($a) {
        return $a % 2 === 0;
    }
);

// Check whether role has access to the operation with custom function

// Returns true
$acl->isAllowed(
    "Guests",
    "Customers",
    "search",
    [
        "a" => 4,
    ]
);

// Returns false
$acl->isAllowed(
    "Guests",
    "Customers",
    "search",
    [
        "a" => 3,
    ]
);

如果你没有向isAllowed的提供第四个参数, 它的默认行为为Acl::ALLOW. 你可以通过setNoArgumentsDefautAction()方法进行修改

use Phalcon\Acl;

<?php
// Set access level for role into resources with custom function
$acl->allow(
    "Guests",
    "Customers",
    "search",
    function ($a) {
        return $a % 2 === 0;
    }
);

// Check whether role has access to the operation with custom function

// Returns true
$acl->isAllowed(
    "Guests",
    "Customers",
    "search"
);

// Change no arguments default action
$acl->setNoArgumentsDefaultAction(
    Acl::DENY
);

// Returns false
$acl->isAllowed(
    "Guests",
    "Customers",
    "search"
);

Objects as role name and resource name

你也可以传递一个对像作为roleName和resourceName. 这个对像的类必须实现Phalcon\Acl\RoleAware(接口) for roleName 或者 Phalcon\Acl\ResourceAware接口(只有一个getResourceName()方法) for resourceName.

我们自己的UserRole类

<?php

use Phalcon\Acl\RoleAware;

// Create our class which will be used as roleName
class UserRole implements RoleAware
{
    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;
    }
}

以下是ModelResource类

<?php

use Phalcon\Acl\ResourceAware;

// Create our class which will be used as resourceName
class ModelResource implements ResourceAware
{
    protected $id;

    protected $resourceName;

    protected $userId;

    public function __construct($id, $resourceName, $userId)
    {
        $this->id           = $id;
        $this->resourceName = $resourceName;
        $this->userId       = $userId;
    }

    public function getId()
    {
        return $this->id;
    }

    public function getUserId()
    {
        return $this->userId;
    }

    // Implemented function from ResourceAware Interface
    public function getResourceName()
    {
        return $this->resourceName;
    }
}

然后你可以在isAllowed()方法中使用

<?php

use UserRole;
use ModelResource;

// Set access level for role into resources
$acl->allow("Guests", "Customers", "search");
$acl->allow("Guests", "Customers", "create");
$acl->deny("Guests", "Customers", "update");

// Create our objects providing roleName and resourceName

$customer = new ModelResource(
    1,
    "Customers",
    2
);

$designer = new UserRole(
    1,
    "Designers"
);

$guest = new UserRole(
    2,
    "Guests"
);

$anotherGuest = new UserRole(
    3,
    "Guests"
);

// Check whether our user objects have access to the operation on model object

// Returns false
$acl->isAllowed(
    $designer,
    $customer,
    "search"
);

// Returns true
$acl->isAllowed(
    $guest,
    $customer,
    "search"
);

// Returns true
$acl->isAllowed(
    $anotherGuest,
    $customer,
    "search"
);

同样,你可以在allow()和deny()方法中的自定义函数访问这些对像.它们在函数中会通过类型,自动绑定到参数.

<?php

use UserRole;
use ModelResource;

// Set access level for role into resources with custom function
$acl->allow(
    "Guests",
    "Customers",
    "search",
    function (UserRole $user, ModelResource $model) { // User and Model classes are necessary
        return $user->getId == $model->getUserId();
    }
);

$acl->allow(
    "Guests",
    "Customers",
    "create"
);

$acl->deny(
    "Guests",
    "Customers",
    "update"
);

// Create our objects providing roleName and resourceName

$customer = new ModelResource(
    1,
    "Customers",
    2
);

$designer = new UserRole(
    1,
    "Designers"
);

$guest = new UserRole(
    2,
    "Guests"
);

$anotherGuest = new UserRole(
    3,
    "Guests"
);

// Check whether our user objects have access to the operation on model object

// Returns false
$acl->isAllowed(
    $designer,
    $customer,
    "search"
);

// Returns true
$acl->isAllowed(
    $guest,
    $customer,
    "search"
);

// Returns false
$acl->isAllowed(
    $anotherGuest,
    $customer,
    "search"
);

你依然可以在isAllowd()方法的第四个参数中传递自定义参数数组。

Roles Inheritance

你可以使用继承,创建更复杂的角色结构. 一个角色可以继承其它角色,这样就可以访问父角色的资源。为了使用角色,你需要在调用addRole()方法时,传递第二参数为要继承的角色。

<?php

use Phalcon\Acl\Role;

// ...

// Create some roles

$roleAdmins = new Role("Administrators", "Super-User role");

$roleGuests = new Role("Guests");

// Add "Guests" role to ACL
$acl->addRole($roleGuests);

// Add "Administrators" role inheriting from "Guests" its accesses
$acl->addRole($roleAdmins, $roleGuests);

Serializing ACL lists

为了提升性能,Phalcon\Acl实例可以被序列化,并保存到APC, session, 文本文件或者数据库表中,然后在加载,而不是需要重新定义整个访问控制列表。如下所示

<?php

use Phalcon\Acl\Adapter\Memory as AclList;

// ...

// Check whether ACL data already exist
if (!is_file("app/security/acl.data")) {
    $acl = new AclList();

    // ... Define roles, resources, access, etc

    // Store serialized list into plain file
    file_put_contents(
        "app/security/acl.data",
        serialize($acl)
    );
} else {
    // Restore ACL object from serialized file
    $acl = unserialize(
        file_get_contents("app/security/acl.data")
    );
}

// Use ACL list as needed
if ($acl->isAllowed("Guests", "Customers", "edit")) {
    echo "Access granted!";
} else {
    echo "Access denied :(";
}

建议在开发阶段使用Memory adapter, 而在生产环境中,使用其它的adapters.

ACL Events

如果存在EventsManager,Phalcon\Acl可以向它发送事件。事件的触发类型为”all”. 当事件处理函数返回false时,则可以停止当前操作。它支持以下的操作

Event nameTriggeredCan stop operation?
beforeCheckAccessTriggered before checking if a role/resource has accessYes
afterCheckAccessTriggered after checking if a role/resource has accessNo
<?php

use Phalcon\Acl\Adapter\Memory as AclList;
use Phalcon\Events\Event;
use Phalcon\Events\Manager as EventsManager;

// ...

// Create an event manager
$eventsManager = new EventsManager();

// Attach a listener for type "acl"
$eventsManager->attach(
    "acl:beforeCheckAccess",
    function (Event $event, $acl) {
        echo $acl->getActiveRole();

        echo $acl->getActiveResource();

        echo $acl->getActiveAccess();
    }
);

$acl = new AclList();

// Setup the $acl
// ...

// Bind the eventsManager to the ACL component
$acl->setEventsManager($eventsManager);

Implementing your own adpaters

你可以实现Phalcon\Acl\AdapterInterface或者继承已经存在的Adapter, 实现自己的Adapter.

Example

基础 1

$di->set('dispatcher', function () {
    $dispatcher = new Dispatcher();
    $dispatcher->setDefaultNamespace('Rx\Controllers');
    $eventsManager = new EventsManager;
    $eventsManager->attach('dispatch:beforeDispatch', new SecurityPlugin());
    $dispatcher->setEventsManager($eventsManager);
    return $dispatcher;
});

在路由执行前,我们先执行SecurityPlugin

namespace Rx\Plugins;

use Phalcon\Events\Event;
use Phalcon\Mvc\User\Plugin;
use Phalcon\Mvc\Dispatcher;
class SecurityPlugin extends Plugin
{
    public function __construct()
    {
    }
    public function beforeDispatch(Event $event, Dispatcher $dispatcher)
    {
        //获取用户访问的controller
        $controllerName = $dispatcher->getControllerName();
        //获取用户request中的action
        $actionName = $dispatcher->getActionName();
        //auth是注册的一个服务,对token或者用户名进行认证, 然后在Session中
        //保存一个auth-identity, 它是由用户id, name, profile组成的一个数组
        //而getIdentity就是返回这样一个数组
        $identity = $this->auth->getIdentity();

        //获取acl服务,这个acl是自定义的类,而不是Phalcon\Acl\Adapter\Memory,
        //你可以查看接下来的源代码中的对它的定义
        //如果资源是定义在acl的privateResource数组中,则为私有资源,而如果是公开的资源
        //则直接返回true
        if (!$this->acl->isPrivate($controllerName)) {
            return true;
        }

        //如果没有登陆,则重定义到登陆页面
        if (!is_array($identity)) {

            $dispatcher->forward(array(
                'controller' => 'session',
                'action' => 'login'
            ));
            return false;
        }

        //如果Session保存的用户信息中有isAdmin, 表示为管理员,则拥有所有的权限
        //直接返回true
        if ($identity['isAdmin']) {
            return true;
        }

            $actionName = $dispatcher->getActionName();
            //如果没有权限,则重定向到403页面
            if(!$this->acl->isAllowed($identity['id'], $controllerName, $actionName)){

                $dispatcher->forward(array(
                    'controller' => 'index',
                    'action'     => 'show403'
                ));

                return false;
            }

    }

}

自定义访问控制,包含对访问的控制,以下保存控制列表保存在文件

namespace Rx\Acl;
use Phalcon\Mvc\User\Component;
use Phalcon\Acl\Adapter\Memory as AclMemory;
use Phalcon\Acl\Role as AclRole;
use PHalcon\Acl\Resource as AclResource;
use Rx\Models\Profiles;
use PHalcon\Mvc\Model\Resultset;
use Rx\Models\Sf;
use Rx\Models\Permission;
use Rx\Util\FileWriter;

class Acl extends Component
{
    private $acl;
    /*
    array(
    2=>
    array(
        0 => "about",
        1 => "product",
        3 => "search"
    ),
    )
    */
    private $accessListFile =  'accesslist.php';
    private $accessList = array();
    private $privateResource = array(
        'index' => array()
    );

    /*
    所有用户都能访问的资源
    */
    private $allUserResource = array(
        'user' => array('changePassword'),
        'index'=> array('index','show403')
    );
    public function __construct(){
        /*
        array(
            'index' => array(),
            'about' => array(),
            'product' = array(),
        );
        */
        $sourcefile = __DIR__ . '/sources.php';
        $accessfile = __DIR__ . '/' . $this->accessListFile;

        //如果不存在$sourcefile, 即Resources列表,则访问数据库中菜单表,sf_url为需要
        //进行访问控制的controller名称,并 并且添加到privateResource资源中
        if (!file_exists($sourcefile)) {
            $sources = Sf::find(array(
                'columns' =>  array('sf_url')
            ));

            foreach($sources as $source){
                $this->privateResource[$source->sf_url] = array();
            }
            FileWriter::writeObject($sourcefile, $this->privateResource, true);
        } else{
            $fileArray = require $sourcefile;
            $this->privateResource = $fileArray;
        }

        /**
        如是果没有访问控制列表文件,则建立
        */
        if (!file_exists($accessfile)) {
            $this->buildAccessList();
        } else {

            $this->accessList = require $accessfile;
        }

    }
    public function isPrivate($controllerName){
         return isset($this->privateResource[$controllerName]);
    }
    /*
    自定义的方法,根据用户的id,确认是否有$action的访问权限 
    */
    public function isAllowed($userId, $controller, $action){
        //如果这个controller是公开的资源,则直接返回
        if(isset($this->allUserResource[$controller])){
            if(in_array($action, $this->allUserResource[$controller])){
                return true;
            }
        }
        //如果用户的id没有出现在访问控制列表里,则直接返回false.
        if (!array_key_exists($userId, $this->accessList)) {
            return false;
        }

        //根据用户id,返回它可以访问的资源, 
        /*
        2=>
        array(
            0 => "about",
            1 => "product",
            3 => "search"
        ),
        */
        $accesses = $this->accessList[$userId];
        //检查request要访问的controller,是否出现在用户可访问的列表controller
        if(in_array($controller, $accesses)){
            return true;
        } else {
            return false;
        }

        .
        .
        .
        //针对Action
    }
    //读取数据库中的访问控制表
    //它由用户ID和controller name组成
    /*
    比如,用户ID 为2的用户,它可以访问的资源为

    array(
    2=>
    array(
        0 => "about",
        1 => "product",
        3 => "search"
    ),
    )

    */
    public  function buildAccessList(){
        $file = __DIR__ . '/' .$this->accessListFile;
        $sql = "SELECT up.us_id, sf.sf_url FROM up INNER JOIN sf where sf.sf_id = up.sf_id and sf.sf_url != ''";
        $results = $this->db->fetchAll($sql);
        $this->accessList = array();
        foreach ($results as $row) {
           $this->accessList[$row['us_id']][] = $row['sf_url'];
        }
        FileWriter::writeObject($file,$this->accessList,true);
    }

}

高级

这个例子是应用于Micro应用, 详细的代码可以参考https://github.com/carloscgo/Phalcon-JWT,首先我们向Micro应用设置EeventManager, 让它监听micro事件, 它的处理器为 AuthorizationMiddleware.

$app->attach("micro", new AuthorizationMiddleware)

以下是AuthorizationMiddleware类

/*
plugin为Phalcon\Mvc\User\Plugin,通过它可以直接获取di中注册的服务

*/
class AuthorizationMiddleware extends Plugin implements MiddlewareInterface
{
    public function beforeExecuteRoute(Event $event, Api $api)
    {
    /*
    $collection是自定义的collection, 它是一个自定义类,源代码在之后的代码中,它继承于
    Phalcon\Mvc\Micro\Collection, 而Phalcon\Mvc\Micro\Collection实现了CollectionInterface(getPrefix, setPrefix, getHandlers, getHandler, get('/about', handler, [endpoint name])
    一个collection API handler进行分组,collection中的项,属于controller中action

    getMatchedCollection()方法,定义在Api类中,它继承于Micro
    public function getMatchedCollection()
    {
        //Collection->createRouteName(), 所以getMatchedRouteNamePart由两部分组成
        //一个是collection, 它的字符串为collection的prefix
        //endpoint则为method+path
        $collectionIdentifier = $this->getMatchedRouteNamePart('collection');

        if (!$collectionIdentifier) {
            return null;
        }
        //collectionsByIdentifier是一个数组,通过Api的mount(collection)方法绑定
        //整个app要用要的collection
        return array_key_exists($collectionIdentifier,
            $this->collectionsByIdentifier) ? $this->collectionsByIdentifier[$collectionIdentifier] : null;
    }

    protected function getMatchedRouteNamePart($key)
    {
        if (is_null($this->matchedRouteNameParts)) {
            //this->getRouter()获取Micro内置的Phalcon\Mvc\Router
            //getMatchedRoute Returns the route that matches the handled URI
            //在Micro.mount(collection)时,getHeaders(每个端点)会获取每个端点的处理函数
            //并且设置每个端点,即路由的名称, 而这个名称是Collection->mount($endpoint)时
            //通过createRouteName(Endpoint $endpoint)设置的,它由一个序列化的数组。
            //可以查看Rest\Api\Collection
            $routeName = $this->getRouter()->getMatchedRoute()->getName();

            if (!$routeName) {
                return null;
            }

            $this->matchedRouteNameParts = @unserialize($routeName);
        }

        if (is_array($this->matchedRouteNameParts) && array_key_exists($key, $this->matchedRouteNameParts)) {
            return $this->matchedRouteNameParts[$key];
        }

        return null;
    }
    */
        $collection = $api->getMatchedCollection();
        $endpoint = $api->getMatchedEndpoint();

        if (!$collection || !$endpoint) {
            return;
        }

        /**
        acl是注册的服务, 它的类为Rest\Acl\Adapter\Memory,它继承于\Phalcon\Acl\Adapter\Memory, 所以有isAllowed方法,而它又实现了MountingEnabledAdapterInterface接口,所有可以mount或者mountMany,生成访问控制列表.
        mount和mountMany, 会在启动AclBootstrap时调用, 以下是mount(Collection)的源代码

public function mount(\PhalconRest\Acl\MountableInterface $mountable)
    {
        if ($this instanceof \Phalcon\Acl\AdapterInterface) {
            //获得这个collection下的资源
            /*
             *
             * [
             *   [ Resources, ['endpoint1', 'endpoint2'] ]
             * ]
             */

            $resources = $mountable->getAclResources();
            /*
             * [
             *   Acl::ALLOW => [['rolename', 'resourcename', 'endpointname], ['rolename', 'resourcename', 'endpointname]],
             *   Acl::DENY => [['rolename', 'resourcename', 'endpointname], ['rolename', 'resourcename', 'endpointname]]
             * ]
             * */
            $rules = $mountable->getAclRules($this->getRoles());

            // Mount resources
            foreach ($resources as $resourceConfig) {

                if (count($resourceConfig) == 0) {
                    continue;
                }
                //添加rosource到访问控制列表
                $this->addResource($resourceConfig[0], count($resourceConfig) > 1 ? $resourceConfig[1] : null);
            }

            // Mount rules
            $allowedRules = array_key_exists(\Phalcon\Acl::ALLOW, $rules) ? $rules[\Phalcon\Acl::ALLOW] : [];
            $deniedRules = array_key_exists(\Phalcon\Acl::DENY, $rules) ? $rules[\Phalcon\Acl::DENY] : [];
            //[['rolename', 'resourcename', 'endpointname], ['rolename', 'resourcename', 'endpointname]]
            foreach ($allowedRules as $ruleConfig) {

                if (count($ruleConfig) < 2) {
                    continue;
                }
                //设置每个角色对资源的访问控制列表
                $this->allow($ruleConfig[0], $ruleConfig[1], count($ruleConfig) > 2 ? $ruleConfig[2] : null);
            }

            foreach ($deniedRules as $ruleConfig) {

                if (count($ruleConfig) < 2) {
                    continue;
                }

                $this->deny($ruleConfig[0], $ruleConfig[1], count($ruleConfig) > 2 ? $ruleConfig[2] : null);
            }
        }
    }
        */
        $allowed = $this->acl->isAllowed($this->userService->getRole(), $collection->getIdentifier(),
            $endpoint->getIdentifier());

        if (!$allowed) {
            throw new Exception(ErrorCodes::ACCESS_DENIED);
        }
    }

    public function call(Micro $api)
    {
        return true;
    }
}
Collection

以下是Collection的定义, 每个Collection有它自己的权限定义,允许或者不允许某个角色的访问, 它是如何进行权限验证的呢?这是因为它实现了Rest\Acl\MountableInterface

interface MountableInterface
{
    /**
     * 按照下面的格式,返回ACL中的资源
     * 这类似于ACLs官方教程中的
     * $customersResource = new Resource("Customers");
     * acl->addResource(customersResource, ["action1", "action2"])
     * 它会生成Customers!action1, Customers!action2
     * 并且分别设置acl->_accessList[Customer!action1]=true;
     * acl->allow("rolename", "Customers", "action1");
     * [
     *   [Resources, ['endpoint1', 'endpoint2']]
     * ]
     * @return array
     */
    public function getAclResources();

    /**
     * 按照下面的格式,返回ACL 中的权限
     * [
     *    Acl::ALLOW => [['rolename', 'resourcename', 'endpointname'], ['rolename', 'resourcename', 'endpointname']],
     *    Acl::DENY => [['rolename', 'resourcename', 'endpointname'], ['rolename', 'resourcename', 'endpointname']]
     * ]
     * @param array $roles
     * @return array
     */
    public function getAclRules(array $roles);
}

/**
 * Class Collection
 * 用于对API handler进行分组,collection中的项,属于controller中action
 * Phalcon\Mvc\Micro\Collection已经实现了CollectionInterface
以下是官方教程中如何使用Collection
use Phalcon\Mvc\Micro\Collection as MicroCollection;

$posts = new MicroCollection();

// Set the main handler. ie. a controller instance
$posts->setHandler(
new PostsController()
);

// Set a common prefix for all routes
$posts->setPrefix("/posts");

// Use the method 'index' in PostsController
$posts->get("/", "index");

// Use the method 'show' in PostsController
$posts->get("/show/{slug}", "show");

$app->mount($posts);
 *
 * @package Rest\Api
 */

class Collection extends \Phalcon\Mvc\Micro\Collection implements MountableInterface
{
    //自定义类添加的属性
    protected $name;
    protected $description;

    //允许访问当前Collection的角色
    protected $allowedRoles = [];
    //拒绝访问当前Collection的角色
    protected $deniedRoles = [];
    //用户保存当前的collection下的端点
    protected $endpointsByName = [];

    /**
     * 构造一个Collection, 并且根据$prefix设置所有api route的前缀
     * Collection constructor.
     * @param $prefix
     */
    public function __construct($prefix)
    {
        parent::setPrefix($prefix);
        $this->initialize();
    }

    protected function initialize()
    {

    }


    public static function factory($prefix, $name = null)
    {
        //获取延迟静态绑定的类名,即子类的名称
        $calledClass = get_called_class();

        $collection = new $calledClass($prefix);

        if($name){
            $collection->name($name);
        }
        return $collection;
    }

    /**
     * 设置这个collection的名称
     * @param $name
     * @return $this
     */
    public function name($name) {
        $this->name = $name;
        return $this;
    }

    public function description($description){
        $this->description = $description;
        return $this;
    }

    public function getDescription()
    {
        return $this->description;
    }


    public function setPrefix($prefix)
    {
       throw new Exception(ErrorCodes::GENERAL_SYSTEM, null, 'Setting prefix after the collection initialization is prohibited')
    }

    /**
     * 设置collection的controller类
     * @param $handler
     * @param bool $lazy
     * @return $this
     */
    public function handler($handler, $lazy = true){
        $this->setHandler($handler, $lazy); // $handler为字符串,惰加载, 而不是直接创建一个Controller类,只在需要的时候加载创建
        return $this;
    }


    /**
     * 加载endpoint到collection
     * @param Endpoint $endpoint
     * @return $this
     */
    public function mount(Endpoint $endpoint)
    {
        $this->endpoint($endpoint);
        return $this;
    }


    /**
     * 加载endpoint到collection
     * @param Endpoint $endpoint
     * @return $this
     */
    public function endpoint(Endpoint $endpoint){
        $this->endpointsByName[$endpoint->getName()] = $endpoint;

        //根据endpoint定义的方法,使用Phalcon\Mvc\Micro\Collection中添加端点
        switch ($endpoint->getHttpMethod()) {
            case HttpMethods::GET:
                $this->get($endpoint->getPath(), $endpoint->getHandlerMethod(), $this->createRouteName($endpoint)); //可选名称
            case HttpMethods::POST:

                $this->post($endpoint->getPath(), $endpoint->getHandlerMethod(), $this->createRouteName($endpoint));
                break;

            case HttpMethods::PUT:

                $this->put($endpoint->getPath(), $endpoint->getHandlerMethod(), $this->createRouteName($endpoint));
                break;

            case HttpMethods::DELETE:

                $this->delete($endpoint->getPath(), $endpoint->getHandlerMethod(), $this->createRouteName($endpoint));
                break;
        }
        return $this;
    }

    public function createRouteName(Endpoint $endpoint)
    {
        return serialize([
            'collection' => $this->getIdentifier(),  //返回prefix
            'endpoint' => $endpoint->getIdentifier(), //返回端点的方和和路径
        ]);
    }

    public function getIdentifier(){
        return $this->getPrefix();
    }

    /**
     * 返回当前collection的所有Endpoint
     * @return array Endpoint
     */
    public function getEndpoints(){
        return array_values($this->endpointsByName);
    }

    /**
     * 根据端点的名字,获得Endpoint
     * example: create, update, remove, all
     * @param $name
     * @return bool
     */
    public function getEndpoint($name) {
        return array_key_exists($name, $this->endpointsByName) ? $this->endpointsByName[$name] : null;
    }
    /**
     * 设置可以访问这个collection的角色,它可以被Endpoint级别覆盖
     * @return $this
     */
    public function allow(){
        $roleNames = func_get_args(); //获取调用这个方法的参数,返回一个数组

        $roleNames = Core::array_flatten($roleNames);

        foreach($roleNames as $role) {
            if (!in_array($role, $this->allowedRoles)) {
                $this->allowedRoles[] = $role;
            }
        }
        return $this;
    }
    /**
     * @return string[] Array of allowed role-names
     */
    public function getAllowedRoles()
    {
        return $this->allowedRoles;
    }

    /***
     * Denies access to this collection for role with the given names. This can be overwritten on the Endpoint level.
     *
     * @param ...array $roleNames Names of the roles to deny
     *
     * @return $this
     */
    public function deny()
    {
        $roleNames = func_get_args();

        // Flatten array to allow array inputs
        $roleNames = Core::array_flatten($roleNames);

        foreach ($roleNames as $role) {

            if (!in_array($role, $this->deniedRoles)) {
                $this->deniedRoles[] = $role;
            }
        }

        return $this;
    }

    /**
     * @return string[] Array of denied role-names
     */
    public function getDeniedRoles()
    {
        return $this->deniedRoles;
    }

     /**
     * 按照下面的格式,返回ACL中的资源
     * [
     *   [Resources, ['endpoint1', 'endpoint2']]
     * ]
     * @return array
     */
    public function getAclResources()
    {
        //访问当前collection中包含的Endpoint, 并且生成一个由Endpoint indentifiers
        //组成的数据, identifiers由endpoint的方法,路径组成
        $apiEndpointIdentifiers = array_map(function (Endpoint $apiEndpoint) {
            return $apiEndpoint->getIdentifier();
        }, $this->endpointsByName);

        //collection当前的标识符为getProfix(), 
        //Resource的构造函数的第一个参数为资源名,第二个为资源描述
        return [
           [new \Phalcon\Acl\Resource($this->getIdentifier(), $this->getName()), $apiEndpointIdentifiers)
        ]
    }

    /*传递一个角色数组,获取它们对当前collection的端点访问权限规则
     * Returns the ACL rules in the following format:
     *
     * [
     *   Acl::ALLOW => [['rolename', 'resourcename', 'endpointname], ['rolename', 'resourcename', 'endpointname]],
     *   Acl::DENY => [['rolename', 'resourcename', 'endpointname], ['rolename', 'resourcename', 'endpointname]]
     * ]
     *
     * @param array $roles The currently registered role on the ACL
     *
     * @return array
     */
    */
    public function getAclRules(array $roles)
    {
        $allowedResponse = [];
        $deniedResponse = [];
        //允许访问的角色数组,可以通过Collection->allow(Role $r)进行添加
        $defaultAllowedRoles = $this->allowedRoles;
        $defaultDeniedRoles = $this->deniedRoles;

        //遍历传入的角色数组 
        foreach($roles as $role){
            foreach($this->endpointsByName as $apiEndpoint){
                $rule = null;
                //如果当前角色是可以访问collection
                if(in_array($role, $defaultAllowedRoles)){
                    $rule = true;
                }

                if(in_array($role, $defaultDeniedRoles)){
                    $rule = false;
                }
                //如果端点设置了哪些角色可以访问, 则覆盖collection的设置
                if (in_array($role, $apiEndpoint->getAllowedRoles())) {
                    $rule = true;
                }

                if (in_array($role, $apiEndpoint->getDeniedRoles())) {
                    $rule = false;
                }
                if ($rule === true) {
                    $allowedResponse[] = [$role, $this->getIdentifier(), $apiEndpoint->getIdentifier()];
                }

                if ($rule === false) {
                    $deniedResponse[] = [$role, $this->getIdentifier(), $apiEndpoint->getIdentifier()];
                }
            }
        }
        //Acl::ALLOW = 1 ACL::DENY = 0 
        return [
            Acl::ALLOW => $allowedResponse,
            Acl::DENY => $deniedResponse
        ];
    }

    public function getName(){
        return $this->name;
    }

}

以下是Endpoint的定义, 每个端点也有它自己的权限定义,允许或者不允许某个角色的访问, Endpoint类主要是用来定义当前端点的路径,名称,描述,处理器,请求这个端点的方法,以及允许访问的角色(或者拒绝), 它并须在Collection对像中Mount加载(根据Endpoint的路径,方法,处理器),否则不会路径找到对应的控制器的Action.

class Endpoint
{
    const ALL = 'all';
    const FIND = 'find';
    const CREATE = 'create';
    const UPDATE = 'update';
    const REMOVE = 'remove';

    protected $name;
    protected $description;

    //Endpoint所对应的方法
    protected $httpMethod;
    protected $path;
    protected $handlerMethod;


    protected $postedDataMethod = PostedDataMethods::AUTO; //json or post 提交数据是使用的方法

    protected $allowedRoles = [];
    protected $deniedRoles = [];

    /**
     * 此工厂函数且于创建一个Endpoint
     * @param string $path
     * @param string $httpMethod 默认为GET方法s
     * @param string $handlerMethod 端点的处理函数,
     * @return Endpoint
     */
    public static function factory($path, $httpMethod = HttpMethods::GET, $handlerMethod = null)
    {
        return new Endpoint($path, $httpMethod, $handlerMethod);
    }


    /**
     * Endpoint constructor.
     * 构造一个api端点,必须传递api的url路径, 请求的方法默认为get, 处理函数为空
     * @param $path
     * @param string $httpMethod
     * @param null $handlerMethod
     */
    public function __construct($path, $httpMethod = HttpMethods::GET, $handlerMethod = null)
    {
        $this->path = $path;
        $this->httpMethod = $httpMethod;
        $this->handlerMethod = $handlerMethod;
    }


    public function description($description) {
        $this->$description = $description;
        return $this;
    }


    public function name($name)
    {
        $this->name = $name;
        return $this;
    }

    /**
     * 返回预配置的所有端点
     * @return mixed
     */
    public static function all(){
        return self::factory('/', HttpMethods::GET, 'all')
            ->name(self::ALL)
            ->description('Returns all items');
    }

    /**
     * 返回预置的create 节点
     * @return $this
     */
    public static function create()
    {
        return self::factory("/", HttpMethods::POST, 'create')
            ->name(self::CREATE)
            ->description("Creates a new item using the posted data");
    }

    public static function update(){
        return self::factory("/{id}", HttpMethods::PUT, 'update')
            ->name(self::UPDATE)
            ->description('Updates an existing item identified by {id}, using the posted data');
    }

    public static function remove(){
        return self::factory("/{id}", HttpMethods::DELETE, 'remove')
            ->name(self::REMOVE)
            ->description("Removes the item identified by {id}");
    }



    /**
     * Returns pre-configured GET endpoint
     *
     * @param $path
     * @param string $handlerMethod
     *
     * @return Endpoint
     */
    public static function get($path, $handlerMethod = null)
    {
        return self::factory($path, HttpMethods::GET, $handlerMethod);
    }

    /**
     * Returns pre-configured POST endpoint
     *
     * @param $path
     * @param string $handlerMethod
     *
     * @return Endpoint
     */
    public static function post($path, $handlerMethod = null)
    {
        return self::factory($path, HttpMethods::POST, $handlerMethod);
    }

    /**
     * Returns pre-configured PUT endpoint
     *
     * @param $path
     * @param string $handlerMethod
     *
     * @return Endpoint
     */
    public static function put($path, $handlerMethod = null)
    {
        return self::factory($path, HttpMethods::PUT, $handlerMethod);
    }

    /**
     * Returns pre-configured DELETE endpoint
     *
     * @param $path
     * @param string $handlerMethod
     *
     * @return Endpoint
     */
    public static function delete($path, $handlerMethod = null)
    {
        return self::factory($path, HttpMethods::DELETE, $handlerMethod);
    }

    /**
     * Returns pre-configured HEAD endpoint
     *
     * @param $path
     * @param string $handlerMethod
     *
     * @return Endpoint
     */
    public static function head($path, $handlerMethod = null)
    {
        return self::factory($path, HttpMethods::HEAD, $handlerMethod);
    }

    /**
     * Returns pre-configured OPTIONS endpoint
     *
     * @param $path
     * @param string $handlerMethod
     *
     * @return Endpoint
     */
    public static function options($path, $handlerMethod = null)
    {
        return self::factory($path, HttpMethods::OPTIONS, $handlerMethod);
    }

    /**
     * Returns pre-configured PATCH endpoint
     *
     * @param $path
     * @param string $handlerMethod
     *
     * @return Endpoint
     */
    public static function patch($path, $handlerMethod = null)
    {
        return self::factory($path, HttpMethods::PATCH, $handlerMethod);
    }

    /**
     * @return string Unique identifier for this endpoint (returns a combination of the HTTP method and the path)
     */
    public function getIdentifier()
    {
        return $this->getHttpMethod() . ' ' . $this->getPath();
    }

    /**
     * @return string HTTP method of the endpoint
     */
    public function getHttpMethod()
    {
        return $this->httpMethod;
    }



    /**
     * @return string Path of the endpoint, relative to the collection
     */
    public function getPath()
    {
        return $this->path;
    }

    /**
     * @param string $handlerMethod Name of controller-method to be called for the endpoint
     *
     * @return static
     */
    public function handlerMethod($handlerMethod)
    {
        $this->handlerMethod = $handlerMethod;
        return $this;
    }

    /**
     * @return string Name of controller-method to be called for the endpoint
     */
    public function getHandlerMethod()
    {
        return $this->handlerMethod;
    }

    /**
     * @return string|null Name of the endpoint
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * @return string Description for the endpoint
     */
    public function getDescription()
    {
        return $this->description;
    }

    /**
     * @return string $method One of the method constants defined in PostedDataMethods
     */
    public function getPostedDataMethod()
    {
        return $this->postedDataMethod;
    }

    /**
     * Sets the posted data method to POST
     *
     * @return static
     */
    public function expectsPostData()
    {
        $this->postedDataMethod(PostedDataMethods::POST);
        return $this;
    }

    /**
     * @param string $method One of the method constants defined in PostedDataMethods
     *
     * @return static
     */
    public function postedDataMethod($method)
    {
        $this->postedDataMethod = $method;
        return $this;
    }

    /**
     * Sets the posted data method to JSON_BODY
     *
     * @return static
     */
    public function expectsJsonData()
    {
        $this->postedDataMethod(PostedDataMethods::JSON_BODY);
        return $this;
    }

    /**
     * 为当前端点,添加可以访问的角色
     * @return $this
     */
    public function allow() {
        $roleNames = func_get_args(); //获取调用这个函数的参数数组
        $roleNames = Core::array_flatten($roleNames); //转换为一维

        foreach ($roleNames as $role) {
            if(!in_array($role, $this->allowRoles)) {
                $this->allowedRoles[] = $role;
            }
        }
        return $this;
    }

    public function getAllowedRoles(){
        return $this->allowedRoles;
    }


    public function deny()
    {
        $roleNames = func_get_args();

        // Flatten array to allow array inputs
        $roleNames = Core::array_flatten($roleNames);

        foreach ($roleNames as $role) {

            if (!in_array($role, $this->deniedRoles)) {
                $this->deniedRoles[] = $role;
            }
        }

        return $this;
    }

    /**
     * @return string[] Array of denied role-names
     */
    public function getDeniedRoles()
    {
        return $this->deniedRoles;
    }
}

在启动一个Micro App时,还是启动CollectionBootstartp, 向App中,添加Collection. 以及资源,然后在执行访问控制时,才能获得所有的Collection信息.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值