Авторизация через 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
Просмотров: 739
Правила перепечатки


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

^ Наверх ^