Авторизация через Middleware

В прошлой статье мы реализовали аутентификацию. В этой займёмся авторизацией.

Реализуем допуск пользователя к определённым ресурсам. Сформируем набор пользовательских групп и реализуем Role-Based Access Control (RBAC).

Для имплементации RBAC воспользуемся zendframework/zend-permissions-rbac.

Стартуем

Для удобства мы создадим новый модуль под названием Permission куда поместим наши классы, посредников и общую конфигурацию.

Установим вспомогательные инструменты:

$ composer require --dev zendframework/zend-expressive-tooling

Далее создадим модуль Permission:

$ ./vendor/bin/expressive module:create Permission

Итак, приступим.

Аутентификация

В этом примере мы задействуем модуль Auth, который реализовали в прошлом уроке. Воспользуемся классом Auth\Action\AuthAction::class для получения данных о аутентифицированном пользователе.

Авторизация

Авторизация будет работать на основе системы RBAC. Роль пользователя — это строка, определяющая уровень доступа; к примеру, для роли administrator открыты все права.

В нашем случае мы будем открывать или закрывать доступ к маршрутам в зависимости от роли пользователя. В терминологии RBAC каждая роль — это определённое право доступа.

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

К примеру мы работаем над созданием блога. Нам понадобятся следующие роли:

  • administrator
  • editor
  • contributor

Для пользователя с ролью contributor будет позволено создавать, редактировать и удалять только те посты, которые были созданы им самим. Для роли editor будут характерны те же операции, вдобавок он сможет публиковать статьи на сайте. Для administrator нет ограничений, он сможет выполнять все вышеперечисленные действия.

Данный пример очень показателен. По факту роль administrator будет иметь те же привилегии что и editor. В свою очередь editor-у будет позволено всё, что позволено для роли contributor.

Составим конфигурационный массив:

// В src/Permission/config/rbac.php:

return [
    'roles' => [
        'administrator' => [],
        'editor'        => ['admin'],
        'contributor'   => ['editor'],
    ],
    'permissions' => [
        'contributor' => [
            'admin.dashboard',
            'admin.posts',
        ],
        'editor' => [
            'admin.publish',
        ],
        'administrator' => [
            'admin.settings',
        ],
    ],
];

Мы определили 3 роли, а также указали кто от кого наследуется.

Все привилегии перечислены в ключе permissions. Для каждой роли перечислены допустимые маршруты, к которым для них будет разрешён доступ.

Для всех ролей открыты маршруты admin.dashboard и admin.posts. Вдобавок у editor-а есть доступ к admin.publish. Для administrator-а открыты все привилегии contributor-а и editor-а. И наконец, только administrator-у будет доступен маршрут admin.settings.

Посредник авторизации

Теперь мы готовы к реализации посредника.

Создадим класс AuthorizationAction в модуле Permission:

// в src/Permission/src/Action/AuthorizationAction.php:

namespace Permission\Action;

use Auth\Action\AuthAction;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface as MiddlewareInterface;
use Permission\Entity\Post as PostEntity;
use Permission\Service\PostService;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\EmptyResponse;
use Zend\Expressive\Router\RouteResult;
use Zend\Permissions\Rbac\AssertionInterface;
use Zend\Permissions\Rbac\Rbac;
use Zend\Permissions\Rbac\RoleInterface;

class AuthorizationAction implements MiddlewareInterface
{
    private $rbac;
    private $postService;

    public function __construct(Rbac $rbac, PostService $postService)
    {
        $this->rbac        = $rbac;
        $this->postService = $postService;
    }

    public function process(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        $user = $request->getAttribute(AuthAction::class, false);
        if (false === $user) {
            return new EmptyResponse(401);
        }

        // if a post attribute is present and user is contributor
        $postUrl = $request->getAttribute('post', false);
        if (false !== $postUrl && $user['role'] === 'contributor') {
            $post = $this->postService->getPost($postUrl);
            $assert = new class ($user['username'], $post) implements AssertionInterface {
                private $post;
                private $username;

                public function __construct(string $username, PostEntity $post)
                {
                    $this->username = $username;
                    $this->post     = $post;
                }

                public function assert(Rbac $rbac)
                {
                    return $this->username === $this->post->getAuthor();
                }
            };
        }

        $route     = $request->getAttribute(RouteResult::class);
        $routeName = $route->getMatchedRoute()->getName();
        if (! $this->rbac->isGranted($user['role'], $routeName, $assert ?? null)) {
            return new EmptyResponse(403);
        }

        return $delegate->process($request);
    }
}

Если пользователь не авторизован, то в атрибуте AuthAction::class будет значение false. В этом случае мы вернём пустой ответ со статусом 401.

Проверка авторизации происходит в методе isGranted($role, $permission) где $role — это роль пользователя ($user['role']), а $permission название маршрута из атрибута RouteResult::class. Если всё ок, то пользователю разрешено выполнить данное действие. В противном случае возвращаем ошибку с кодом 403.

Если текущий пользователь contributor, то мы проверяем редактирует ли он свой пост. Проверка осуществляется по автору поста.

Это позволит быть спокойным в плане того, что contributor сможет редактировать или удалять только свои посты. Данная проверка осуществляется в анонимном классе; мы проверяем идентичность поля username и имя аутентифицированного пользователя. Получаем его имя через объект класса PostEntity, а именно через метод getAuthor().

Чтобы вытащить текущий маршрут, обращаемся к атрибуту RouteResult::class Expressive.

Посредник AuthorizationAction принимает объект Rbac и PostService. Первая зависимость — это инстанс Zend\Permissions\Rbac\Rbac, а вторая сервис для взаимодействия с постом.

Далее создадим фабрику для этого посредника AuthorizationFactory:

namespace Permission\Action;

use Interop\Container\ContainerInterface;
use Zend\Permissions\Rbac\Rbac;
use Zend\Permissions\Rbac\Role;
use Permission\Service\PostService;
use Exception;

class AuthorizationFactory
{
    public function __invoke(ContainerInterface $container)
    {
        $config = $container->get('config');
        if (! isset($config['rbac']['roles'])) {
            throw new Exception('Rbac roles are not configured');
        }
        if (!isset($config['rbac']['permissions'])) {
            throw new Exception('Rbac permissions are not configured');
        }

        $rbac = new Rbac();
        $rbac->setCreateMissingRoles(true);

        // роли
        foreach ($config['rbac']['roles'] as $role => $parents) {
            $rbac->addRole($role, $parents);
        }

        // привилегии
        foreach ($config['rbac']['permissions'] as $role => $permissions) {
            foreach ($permissions as $perm) {
                $rbac->getRole($role)->addPermission($perm);
            }
        }
        $post = $container->get(PostService::class);

        return new AuthorizationAction($rbac, $post);
    }
}

В данном классе мы создаём объект Rbac и запихиваем в него данные из src/Permission/config/rbac.php.

Завершаем работу над модулем Permission, добавив нужные нам зависимости:

// В src/Permission/src/ConfigProvider.php:

// Обновляем метод:
public function __invoke()
{
    return [
        'dependencies' => $this->getDependencies(),
        'rbac'         => include __DIR__ . '/../config/rbac.php',
    ];
}

public function getDependencies()
{
    return [
        'factories' => [
            Service\PostService::class => Service\PostFactory::class,
            Action\AuthorizationAction::class => Action\AuthorizationFactory::class,
        ],
    ];
}

Конфигурация маршрута авторизации

Чтобы активировать авторизацию для конкретного маршрута, впишем посредника Permission\Action\AuthorizationAction в цепочку:

$app->get('/admin/dashboard', [
    Auth\Action\AuthAction::class,
    Permission\Action\AuthorizationAction::class,
    Admin\Action\DashboardAction::class
], 'admin.dashboard');

Маршрут GET /admin/dashboard будет называться admin.dashboard. Добавляем AuthAction и AuthorizationAction перед вызовом DashboardAction. Порядок выполнения посредников очень важен.

Добавьте посредника AuthorizationAction в каждый маршрут, требующий авторизацию.

Итог

В этой статье мы рассмотрели процесс реализации авторизации в простом приложении через посреднические классы.

Данный урок подготовлен для вас командой сайта ruseller.com
Источник урока: https://framework.zend.com/blog/2017-05-04-authorization-middleware.html
Перевел: Станислав Протасевич
Урок создан: 5 Мая 2017
Просмотров: 1662
Правила перепечатки


5 последних уроков рубрики "PHP"

  • Фильтрация данных с помощью zend-filter

    Когда речь идёт о безопасности веб-сайта, то фраза "фильтруйте всё, экранируйте всё" всегда будет актуальна. Сегодня поговорим о фильтрации данных.

  • Контекстное экранирование с помощью zend-escaper

    Обеспечение безопасности веб-сайта — это не только защита от SQL инъекций, но и протекция от межсайтового скриптинга (XSS), межсайтовой подделки запросов (CSRF) и от других видов атак. В частности, вам нужно очень осторожно подходить к формированию HTML, CSS и JavaScript кода.

  • Подключение Zend модулей к Expressive

    Expressive 2 поддерживает возможность подключения других ZF компонент по специальной схеме. Не всем нравится данное решение. В этой статье мы расскажем как улучшили процесс подключение нескольких модулей.

  • Совет: отправка информации в Google Analytics через API

    Предположим, что вам необходимо отправить какую-то информацию в Google Analytics из серверного скрипта. Как это сделать. Ответ в этой заметке.

  • Подборка PHP песочниц

    Подборка из нескольких видов PHP песочниц. На некоторых вы в режиме online сможете потестить свой код, но есть так же решения, которые можно внедрить на свой сайт.

^ Наверх ^