A free and open-source book on ZF3 for beginners


17.10. Реализация RbacManager

Следующее, что мы обсудим, это набор функций для создания контейнера Rbac, целью которого является загрузка иерархии ролей из базы данных и кэширование данных в кэше файловой системы.

Кэш позволяет хранить часто используемые данные в быстродействующей памяти. Например, извлечение ролей и привилегий из базы данных может быть довольно медленным, в то время как хранение заранее вычисленной иерархии ролей может быть быстрее.

17.10.1. Настройка кеширования

Для начала давайте настроим кэширование. Для этого вам нужно установить компоненты Zend\Cache и Zend\Serializer` с помощью следующих команд:

php composer.phar require zendframework/zend-cache
php composer.phar require zendframework/zend-serializer

Затем измените файл config/autoload/global.php, добавив следующие строки:

use Zend\Cache\Storage\Adapter\Filesystem;

return [
    //...
    // Настройка кэша.
    'caches' => [
        'FilesystemCache' => [
            'adapter' => [
                'name'    => Filesystem::class,
                'options' => [
                    // Store cached data in this directory.
                    'cache_dir' => './data/cache',
                    // Store cached data for 1 hour.
                    'ttl' => 60*60*1 
                ],
            ],
            'plugins' => [
                [
                    'name' => 'serializer',
                    'options' => [                        
                    ],
                ],
            ],
        ],
    ],
    //...
];

Это позволит вам использовать кэш Filesystem и хранить кэшированные данные в каталоге APP_DIR/data/cache.

Если вы хотите больше узнать о кэшировании, пожалуйста, обратитесь к документации компонента ZF3 Zend\Cache.

17.10.2. Пишем сервис RbacManager

Функциональным назначением сервиса RbacManager будет создание контейнера Rbac и загрузка ролей и привилегий из базы данных. Если нужная информация уже была сохранена в кэш, сервис загрузит ее оттуда, а не из БД.

Другими задачами сервиса RbacManager будут использование менеджера утверждений, написанного нами ранее, и проверка на динамические утверждения.

Класс RbacManager будет иметь два метода:

Класс RbacManager будет считывать конфигурацию и искать ключ rbac_manager. Этот ключ должен содержать подключ assertions, в котором вы можете зарегистрировать все имеющиеся у вас менеджеры утверждений.

return [
    //...
    
    // Этот ключ хранит конфигурацию для менеджера RBAC. 
    'rbac_manager' => [
        'assertions' => [Service\RbacAssertionManager::class],
    ],
];

Код класса RbacManager, "живущего" в пространстве имен User\Service, представлен ниже.

<?php
namespace User\Service;

use Zend\Permissions\Rbac\Rbac;
use Zend\Permissions\Rbac\Role as RbacRole;
use User\Entity\User;
use User\Entity\Role;
use User\Entity\Permission;

/**
 * Этот сервис отвечает за инициализацию RBAC (Role-Based Access Control – контроль доступа на основе ролей).
 */
class RbacManager 
{
    /**
     * Менеджер сущностей Doctrine.
     * @var Doctrine\ORM\EntityManager
     */
    private $entityManager; 
    
    /**
     * Сервис RBAC.
     * @var Zend\Permissions\Rbac\Rbac
     */
    private $rbac;
    
    /**
     * Сервис аутентификации.
     * @var Zend\Authentication\AuthenticationService 
     */
    private $authService;
    
    /**
     * Кэш файловой системы.
     * @var Zend\Cache\Storage\StorageInterface
     */
    private $cache;
    
    /**
     * Менеджеры утверждений.
     * @var array
     */
    private $assertionManagers = [];
    
    /**
     * Конструирует сервис.
     */
    public function __construct($entityManager, $authService, $cache, $assertionManagers) 
    {
        $this->entityManager = $entityManager;
        $this->authService = $authService;
        $this->cache = $cache;
        $this->assertionManagers = $assertionManagers;
    }
    
    /**
     * Инициализирует контейнер RBAC.
     */
    public function init($forceCreate = false)
    {
        if ($this->rbac!=null && !$forceCreate) {
            // Уже инициализирован; ничего не делаем.
            return;
        }
        
        // Если пользователь хочет, чтобы мы заново инициализировали контейнер RBAC, очищаем кэш.
        if ($forceCreate) {
            $this->cache->removeItem('rbac_container');
        }
        
        // Пробуем загрузить контейнер Rbac из кэша.
        $this->rbac = $this->cache->getItem('rbac_container', $result);
        if (!$result)
        {
            // Создаем контейнер Rbac.
            $rbac = new Rbac();
            $this->rbac = $rbac;

            // Конструируем иерархию ролей, загружая роли и привилегии из базы данных.

            $rbac->setCreateMissingRoles(true);

            $roles = $this->entityManager->getRepository(Role::class)
                    ->findBy([], ['id'=>'ASC']);
            foreach ($roles as $role) {

                $roleName = $role->getName();

                $parentRoleNames = [];
                foreach ($role->getParentRoles() as $parentRole) {
                    $parentRoleNames[] = $parentRole->getName();
                }

                $rbac->addRole($roleName, $parentRoleNames);

                foreach ($role->getPermissions() as $permission) {
                    $rbac->getRole($roleName)->addPermission($permission->getName());
                }
            }
            
            // Сохраняем контейнер Rbac в кэш.
            $this->cache->setItem('rbac_container', $rbac);
        }
    }
    
    /**
     * Проверяет, есть ли привилегия у данного пользователя.
     * @param User|null $user
     * @param string $permission
     * @param array|null $params
     */
    public function isGranted($user, $permission, $params = null)
    {
        if ($this->rbac==null) {
            $this->init();
        }
        
        if ($user==null) {
            
            $identity = $this->authService->getIdentity();
            if ($identity==null) {
                return false;
            }
            
            $user = $this->entityManager->getRepository(User::class)
                    ->findOneByEmail($identity);
            if ($user==null) {
                // Упс... Эта личность есть в сессии, но в базе данных такого пользователя не существует.
                // Мы генерируем исключение, так как, возможно, это является проблемой безопасности.
                throw new \Exception('There is no user with such identity');
            }
        }
        
        $roles = $user->getRoles();
        
        foreach ($roles as $role) {
            if ($this->rbac->isGranted($role->getName(), $permission)) {
                
                if ($params==null)
                    return true;
                    
                foreach ($this->assertionManagers as $assertionManager) {
                    if ($assertionManager->assert($this->rbac, $permission, $params))
                        return true;
                }
            }
            
            $parentRoles = $role->getParentRoles();
            foreach ($parentRoles as $parentRole) {
                if ($this->rbac->isGranted($parentRole->getName(), $permission)) {
                    return true;
                }
            }
        }
        
        return false;
    }
}

Фабрика для класса RbacManager будет выглядеть следующим образом:

<?php
namespace User\Service\Factory;

use Interop\Container\ContainerInterface;
use User\Service\RbacManager;
use Zend\Authentication\AuthenticationService;

/**
 * Это фабричный класс для сервиса RbacManager. Целями данной фабрики являются
 * инстанцирование сервиса и передача ему зависимостей (внедрение зависимостей).
 */
class RbacManagerFactory
{
    /**
     * Этот метод создает сервис RbacManager и возвращает его экземпляр. 
     */
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {        
        $entityManager = $container->get('doctrine.entitymanager.orm_default');
        $authService = $container->get(\Zend\Authentication\AuthenticationService::class);
        $cache = $container->get('FilesystemCache');
        
        $assertionManagers = [];
        $config = $container->get('Config');
        if (isset($config['rbac_manager']['assertions'])) {
            foreach ($config['rbac_manager']['assertions'] as $serviceName) {
                $assertionManagers[$serviceName] = $container->get($serviceName);
            }
        }
        
        return new RbacManager($entityManager, $authService, $cache, $assertionManagers);
    }
}

Top