Phalcon\Acl提供了一个简单和轻量的权限控制管理, 访问控制列表(ACL Access Control List)允许应用控制客户端的请求(request)对它资源(区域)的访问。建议你了解更多关于ACL的相关知识,以便熟悉我们接下来讲到的一些概念.
总体来说,ACLs句含 roles 和 resources. Resources为ACLs定义了需要访问权限的对像. Roles则是ACL对请求访问资源允许还是拒绝的对像.
这个组件最初的设计是在内存中工作。考虑的是易于使用和对访问列表的访问速度上。以下是一个使用 memory adapter的例子
use Phalcon\Acl\Adapter\Memory as AclList;
$acl = new AclList();
默认的,如果没有定义, Phalcon\Acl允许资源中对像中的action。为了提高访问控制列表的安全级别,我们应该设置”deny”级别
use Phalcon\Acl;
// Default action is deny access
$acl->setDefaultAction(
Acl::DENY
);
role是一个对像(字段只有名称和描述),它可以访问或者不能访问access list 中的某些资源。举一个例子, 我们将定义按照一个组织(公司)的人员结构来定义角色. Phalcon\Acl\Role类可以更结构化(固定字段)的方式来创建角色(也可以直接定义数组).
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");
Resource是可以控制访问的对像(字段只有名称和描述)。正常的,MVC应用程序各,Resources指的是contrllers. 但这不是强制性的,我们可以通过Phalcon\Acl\Resource类用来定义资源。在这里,最重要的是将相关的controller中的action或者操作添加到resource. 这样ACL才能知道,哪些资源需要进行控制.
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",
]
);
现在我们拥有了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()则相反
一旦访问控制列表定义完成。我们可以查询它,以确认角色是事有相关的权限
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");
你可以在allow()和deny()方法的第4个参数中添加自定义函数,但它必须返回一个boolean值。这个函数将在使用isAllowed()方法时。你可以在isAllowed()的第4个参数中传递一个数组,这个数组作为定义函数的参数,数组中的key作为自定义函数的参数名。
// 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;
// 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"
);
你也可以传递一个对像作为roleName和resourceName. 这个对像的类必须实现Phalcon\Acl\RoleAware(接口) for roleName 或者 Phalcon\Acl\ResourceAware接口(只有一个getResourceName()方法) for resourceName.
我们自己的UserRole类
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类
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()方法中的自定义函数访问这些对像.它们在函数中会通过类型,自动绑定到参数.
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()方法的第四个参数中传递自定义参数数组。
你可以使用继承,创建更复杂的角色结构. 一个角色可以继承其它角色,这样就可以访问父角色的资源。为了使用角色,你需要在调用addRole()方法时,传递第二参数为要继承的角色。
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);
为了提升性能,Phalcon\Acl实例可以被序列化,并保存到APC, session, 文本文件或者数据库表中,然后在加载,而不是需要重新定义整个访问控制列表。如下所示
...
// 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.
如果存在EventsManager,Phalcon\Acl可以向它发送事件。事件的触发类型为”all”. 当事件处理函数返回false时,则可以停止当前操作。它支持以下的操作
Event name | Triggered | Can stop operation? |
---|---|---|
beforeCheckAccess | Triggered before checking if a role/resource has access | Yes |
afterCheckAccess | Triggered after checking if a role/resource has access | No |
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);
你可以实现Phalcon\Acl\AdapterInterface或者继承已经存在的Adapter, 实现自己的Adapter.
$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有它自己的权限定义,允许或者不允许某个角色的访问, 它是如何进行权限验证的呢?这是因为它实现了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信息.