Не секрет, что ACL (access control lists) могут быть достаточно сложны в использовании. Поскольку Symfony рекомендует избирателей (voters) в качестве альтернативы ACL, я недавно решил, что напишу свой собственный простой в использовании Symfony 5 бандл для управления списками контроля доступа (ACL) в моих приложениях.
programarivm/easy-acl-bundle
изначально был написан
для использования в JWT-аутентифицированном API для одностраничных
приложений (single page applications SPA), но он также может быть
полезен в ряде других сценариев, когда не требуется Security
компонент что в большинстве случаев, по моему скромному мнению,
особенно подходит для сеансов обработки многостраничных приложений
(multi-page applications MPA).EasyAclBundle
полностью полагается на сущности и репозитории Doctrine, что означает, что разрешения просто хранятся в базе данных без привязки к архитектуре вашего приложения.
Тем не менее, вот как легко JWT-токены аутентифицируются и авторизуются в подписчике событий с помощью так называемых easy ACL-репозиториев.
// src/EventSubscriber/TokenSubscriber.phpnamespace App\EventSubscriber;use App\Controller\AccessTokenController;use Doctrine\ORM\EntityManagerInterface;use Firebase\JWT\JWT;use Symfony\Component\EventDispatcher\EventSubscriberInterface;use Symfony\Component\HttpKernel\Event\ControllerEvent;use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;use Symfony\Component\HttpKernel\KernelEvents;class TokenSubscriber implements EventSubscriberInterface{ public function __construct(EntityManagerInterface $em) { $this->em = $em; } public function onKernelController(ControllerEvent $event) { $controller = $event->getController(); // когда класс контроллера определяет несколько action методов, контроллер // возвращается как [$controllerInstance, 'methodName'] if (is_array($controller)) { $controller = $controller[0]; } if ($controller instanceof AccessTokenController) { $jwt = substr($event->getRequest()->headers->get('Authorization'), 7); try { $decoded = JWT::decode($jwt, getenv('JWT_SECRET'), ['HS256']); } catch (\Exception $e) { throw new AccessDeniedHttpException('Whoops! Access denied.'); } $user = $this->em->getRepository('App:User') ->findOneBy(['id' => $decoded->sub]); $identity = $this->em->getRepository('EasyAclBundle:Identity') ->findBy(['user' => $user]); $rolename = $identity[0]->getRole()->getName(); $routename = $event->getRequest()->get('_route'); $isAllowed = $this->em->getRepository('EasyAclBundle:Permission') ->isAllowed($rolename, $routename); if (!$isAllowed) { throw new AccessDeniedHttpException('Whoops! Access denied.'); } } } public static function getSubscribedEvents() { return [ KernelEvents::CONTROLLER => 'onKernelController', ]; }}
Большая часть этого кода не требует объяснения, если вы опытный разработчик; в основном, если входящий токен доступа успешно декодирован, что означает, что данный пользователь аутентифицирован, код пытается выяснить, имеет ли он права для доступа к текущему маршруту.
...$user = $this->em->getRepository('App:User') ->findOneBy(['id' => $decoded->sub]);$identity = $this->em->getRepository('EasyAclBundle:Identity') ->findBy(['user' => $user]);$rolename = $identity[0]->getRole()->getName();$routename = $event->getRequest()->get('_route');$isAllowed = $this->em->getRepository('EasyAclBundle:Permission') ->isAllowed($rolename, $routename);...
Достаточно только двух easy ACL-репозиториев (
Identity
и Permission
) для определения того, может ли
пользователь получить доступ к текущему маршруту.Конфигурация
Теперь давайте посмотрим в чем же вся магия. В целом, все заключается в определении маршрутов вашего приложения:
# config/routes.yamlapi_post_create: path: /api/posts controller: App\Controller\Post\CreateController::index methods: POSTapi_post_delete: path: /api/posts/{id} controller: App\Controller\Post\DeleteController::index methods: DELETEapi_post_edit: path: /api/posts/{id} controller: App\Controller\Post\EditController::index methods: PUT
А также разрешений:
# config/packages/programarivm_easy_acl.yamlprogramarivm_easy_acl: target: App\Entity\User permission: - role: Superadmin routes: - api_post_create - api_post_delete - api_post_edit - role: Admin routes: - api_post_create - api_post_edit - role: Basic routes: - api_post_create
Итак, теперь, если ваша схема базы данных обновлена:
php bin/console doctrine:schema:update --force
Четыре пустые таблицы будут добавлены в вашу базу данных:
easy_acl_identity
easy_acl_permission
easy_acl_role
easy_acl_route
Эта четверка идет рука об руку со следующими сущностями:
Programarivm\EasyAclBundle\Entity\Identity
Programarivm\EasyAclBundle\Entity\Permission
Programarivm\EasyAclBundle\Entity\Role
Programarivm\EasyAclBundle\Entity\Route
И репозиториями:
-
Programarivm\EasyAclBundle\Repository\IdentityRepository
-
Programarivm\EasyAclBundle\Repository\PermissionRepository
-
Programarivm\EasyAclBundle\Repository\RoleRepository
-
Programarivm\EasyAclBundle\Repository\RouteRepository
Наконец, консольная команда
easy-acl:setup
предназначена для заполнения таблиц easy ACL.
php bin/console easy-acl:setupThis will reset the ACL. Are you sure to continue? (y) y
Консоль MySQL:
mysql> select * from easy_acl_identity;Empty set (0.01 sec)mysql> select * from easy_acl_permission;+----+------------+-----------------+| id | rolename | routename |+----+------------+-----------------+| 1 | Superadmin | api_post_create || 2 | Superadmin | api_post_delete || 3 | Superadmin | api_post_edit || 4 | Admin | api_post_create || 5 | Admin | api_post_edit || 6 | Basic | api_post_create |+----+------------+-----------------+6 rows in set (0.00 sec)mysql> select * from easy_acl_role;+----+------------+| id | name |+----+------------+| 1 | Superadmin || 2 | Admin || 3 | Basic |+----+------------+3 rows in set (0.00 sec)mysql> select * from easy_acl_route;+----+-----------------+---------+-----------------+| id | name | methods | path |+----+-----------------+---------+-----------------+| 1 | api_post_create | POST | /api/posts || 2 | api_post_delete | DELETE | /api/posts/{id} || 3 | api_post_edit | PUT | /api/posts/{id} |+----+-----------------+---------+-----------------+3 rows in set (0.00 sec)
Добавление идентификаторов пользователей
Концепция идентификаторов пользователей позволяет пакету вообще не вмешиваться в вашу базу данных, которая не изменяется им.
Как вы можете видеть, три
EasyAcl
таблицы заполнены
данными, но, конечно же, это ваша задача динамически определять
идентификационные данные своих пользователей, как в примере,
показанном ниже.
// src/DataFixtures/EasyAcl/IdentityFixtures.phpnamespace App\DataFixtures\EasyAcl;use App\DataFixtures\UserFixtures;use Doctrine\Bundle\FixturesBundle\Fixture;use Doctrine\Bundle\FixturesBundle\FixtureGroupInterface;use Doctrine\Common\DataFixtures\DependentFixtureInterface;use Doctrine\Common\Persistence\ObjectManager;use Programarivm\EasyAclBundle\EasyAcl;use Programarivm\EasyAclBundle\Entity\Identity;class IdentityFixtures extends Fixture implements FixtureGroupInterface, DependentFixtureInterface{ private $easyAcl; public function __construct(EasyAcl $easyAcl) { $this->easyAcl = $easyAcl; } public function load(ObjectManager $manager) { for ($i = 0; $i < UserFixtures::N; $i++) { $index = rand(0, count($this->easyAcl->getPermission())-1); $user = $this->getReference("user-$i"); $role = $this->getReference("role-$index"); $manager->persist( (new Identity()) ->setUser($user) ->setRole($role) ); } $manager->flush(); } public static function getGroups(): array { return [ 'easy-acl', ]; } public function getDependencies(): array { return [ RoleFixtures::class, UserFixtures::class, ]; }}
Для получения более подробной информации читайте документацию, которая проведет вас через процесс установки и настройки бандла easy ACL.
На этом все. Был ли этот пост полезен? Я надеюсь, что да. Расскажите нам в комментариях ниже!
Возможно, вас также заинтересует...
- Запись CASL React Abilities в JSON-файл с помощью команды Laravel Artisan.
- Сеанс SPA GUI как Non-HttpOnly Cookie
- Совет для ленивых разработчиков Symfony.
Узнать о курсе подробнее.